Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ namespace VirtualClient.Actions

[TestFixture]
[Category("Unit")]
public class PowershellExecutorTests
public class PowerShellExecutorTests
{
private static readonly string ExamplesDirectory = MockFixture.GetDirectory(typeof(PowershellExecutorTests), "Examples", "ScriptExecutor");
private static readonly string ExamplesDirectory = MockFixture.GetDirectory(typeof(PowerShellExecutorTests), "Examples", "ScriptExecutor");

private MockFixture fixture;
private DependencyPath mockPackage;
Expand All @@ -35,7 +35,7 @@ public void SetupTest(PlatformID platform = PlatformID.Win32NT)

this.fixture.Dependencies.RemoveAll<IEnumerable<IBlobManager>>();

this.exampleResults = File.ReadAllText(Path.Combine(PowershellExecutorTests.ExamplesDirectory, "validJsonExample.json"));
this.exampleResults = File.ReadAllText(Path.Combine(PowerShellExecutorTests.ExamplesDirectory, "validJsonExample.json"));

this.fixture.FileSystem.Setup(fe => fe.File.Exists(It.IsAny<string>()))
.Returns(true);
Expand Down Expand Up @@ -127,46 +127,92 @@ public void SetupTest(PlatformID platform = PlatformID.Win32NT)

this.fixture.Parameters = new Dictionary<string, IConvertible>()
{
{ nameof(PowershellExecutor.PackageName), "workloadPackage" },
{ nameof(PowershellExecutor.Scenario), "GenericScriptWorkload" },
{ nameof(PowershellExecutor.CommandLine), "parameter1 parameter2" },
{ nameof(PowershellExecutor.ScriptPath), "genericScript.ps1" },
{ nameof(PowershellExecutor.LogPaths), "*.log;*.txt;*.json" },
{ nameof(PowershellExecutor.ToolName), "GenericTool" },
{ nameof(PowershellExecutor.UsePwsh), false }
{ nameof(PowerShellExecutor.PackageName), "workloadPackage" },
{ nameof(PowerShellExecutor.Scenario), "GenericScriptWorkload" },
{ nameof(PowerShellExecutor.CommandLine), "parameter1 parameter2" },
{ nameof(PowerShellExecutor.ScriptPath), "genericScript.ps1" },
{ nameof(PowerShellExecutor.LogPaths), "*.log;*.txt;*.json" },
{ nameof(PowerShellExecutor.ToolName), "GenericTool" }
};

this.fixture.ProcessManager.OnCreateProcess = (command, arguments, directory) => this.fixture.Process;
}

[Test]
[TestCase(PlatformID.Win32NT, @"\win-x64", @"genericScript.ps1", true, false, "powershell")]
[TestCase(PlatformID.Win32NT, @"\win-x64", @"genericScript.ps1", false, false, "powershell")]
[TestCase(PlatformID.Win32NT, @"\win-x64", @"genericScript.ps1", true, true, "pwsh")]
[TestCase(PlatformID.Win32NT, @"\win-x64", @"genericScript.ps1", false, true, "pwsh")]
[TestCase(@"genericScript.ps1", "powershell")]
[TestCase(@"genericScript.ps1", "powershell")]
[TestCase(@"genericScript.ps1", "powershell")]
[TestCase(@"genericScript.ps1", "powershell.exe")]
[TestCase(@"genericScript.ps1", "PowerShell.exe")]
[TestCase(@"genericScript.ps1", @"C:\Any\Custom\Location\powershell.exe")]
[TestCase(@"genericScript.ps1", "pwsh")]
[TestCase(@"genericScript.ps1", "pwsh.exe")]
[TestCase(@"genericScript.ps1", @"C:\Any\Custom\Location\pwsh.exe")]
[Platform(Exclude = "Unix,Linux,MacOsX")]
public async Task PowershellExecutorExecutesTheCorrectWorkloadCommands(PlatformID platform, string platformSpecificPath, string command, bool runElevated, bool usePwsh, string executorType)
public async Task PowershellExecutorExecutesTheCorrectWorkloadCommands_Windows_Scenarios(string command, string executable)
{
this.SetupTest(platform);
this.fixture.Parameters[nameof(PowershellExecutor.RunElevated)] = runElevated;
this.fixture.Parameters[nameof(PowershellExecutor.ScriptPath)] = command;
this.fixture.Parameters[nameof(PowershellExecutor.UsePwsh)] = usePwsh;
this.SetupTest(PlatformID.Win32NT);
this.fixture.Parameters[nameof(PowerShellExecutor.ScriptPath)] = command;
this.fixture.Parameters[nameof(PowerShellExecutor.Executable)] = executable;

string fullCommand = $"{this.mockPackage.Path}{platformSpecificPath}\\{command} parameter1 parameter2";
string fullCommand = $"{this.mockPackage.Path}\\win-x64\\{command} parameter1 parameter2";

using (TestPowershellExecutor executor = new TestPowershellExecutor(this.fixture))
using (TestPowerShellExecutor executor = new TestPowerShellExecutor(this.fixture))
{
bool commandExecuted = false;
await executor.InitializeAsync(EventContext.None, CancellationToken.None);
string workingDirectory = executor.ExecutableDirectory;

string expectedCommand = $"{executable} -ExecutionPolicy Bypass -NoProfile -NonInteractive -WindowStyle Hidden -Command \"cd '{workingDirectory}';{fullCommand}\"";
this.fixture.ProcessManager.OnCreateProcess = (exe, arguments, workingDirectory) =>
{
if(expectedCommand == $"{exe} {arguments}")
{
commandExecuted = true;
}

return new InMemoryProcess
{
StartInfo = new ProcessStartInfo
{
FileName = exe,
Arguments = arguments
},
ExitCode = 0,
OnStart = () => true,
OnHasExited = () => true
};
};

await executor.InitializeAsync(EventContext.None, CancellationToken.None)
.ConfigureAwait(false);
await executor.ExecuteAsync(CancellationToken.None);

Assert.DoesNotThrowAsync(() => executor.ExecuteAsync(CancellationToken.None));
Assert.IsTrue(commandExecuted);
}
}

[Test]
[TestCase("genericScript.ps1", "pwsh")]
[TestCase("genericScript.ps1", @"/home/any/custom/location/pwsh")]
[TestCase("genericScript.ps1", "sudo pwsh")]
public async Task PowershellExecutorExecutesTheCorrectWorkloadCommands_Unix_Scenarios(string command, string executable)
{
this.SetupTest(PlatformID.Unix);
this.fixture.Parameters[nameof(PowerShellExecutor.ScriptPath)] = command;
this.fixture.Parameters[nameof(PowerShellExecutor.Executable)] = executable;

string fullCommand = $"{this.mockPackage.Path}/linux-x64/{command} parameter1 parameter2";

using (TestPowerShellExecutor executor = new TestPowerShellExecutor(this.fixture))
{
bool commandExecuted = false;
await executor.InitializeAsync(EventContext.None, CancellationToken.None);
string workingDirectory = executor.ExecutableDirectory;

string expectedCommand = $"{executorType} -ExecutionPolicy Bypass -NoProfile -NonInteractive -WindowStyle Hidden -Command \"cd '{workingDirectory}';{fullCommand}\"";
string expectedCommand = $"{executable} -ExecutionPolicy Bypass -NoProfile -NonInteractive -WindowStyle Hidden -Command \"cd '{workingDirectory}';{fullCommand}\"";
this.fixture.ProcessManager.OnCreateProcess = (exe, arguments, workingDirectory) =>
{
if(expectedCommand == $"{exe} {arguments}")
if (expectedCommand == $"{exe} {arguments}")
{
commandExecuted = true;
}
Expand All @@ -184,37 +230,51 @@ await executor.InitializeAsync(EventContext.None, CancellationToken.None)
};
};

await executor.ExecuteAsync(CancellationToken.None)
.ConfigureAwait(false);
await executor.ExecuteAsync(CancellationToken.None);

Assert.DoesNotThrowAsync(() => executor.ExecuteAsync(CancellationToken.None));
Assert.IsTrue(commandExecuted);
}
}

[Test]
[TestCase(PlatformID.Win32NT, @"\win-x64\")]
public void PowershellExecutorDoesNotThrowWhenTheWorkloadDoesNotProduceValidMetricsFile(PlatformID platform, string platformSpecificPath)
public void PowershellExecutorDoesNotThrowWhenTheWorkloadDoesNotProduceValidMetricsFile()
{
this.SetupTest(platform);
this.fixture.File.Setup(fe => fe.Exists($"{this.mockPackage.Path}{platformSpecificPath}test-metrics.json"))
this.SetupTest(PlatformID.Win32NT);
this.fixture.File.Setup(fe => fe.Exists($@"{this.mockPackage.Path}\win-x64\test-metrics.json"))
.Returns(false);

using (TestPowershellExecutor executor = new TestPowershellExecutor(this.fixture))
using (TestPowerShellExecutor executor = new TestPowerShellExecutor(this.fixture))
{
this.fixture.ProcessManager.OnCreateProcess = (command, arguments, directory) => this.fixture.Process;

Assert.DoesNotThrowAsync(() => executor.ExecuteAsync(CancellationToken.None));
}
}

private class TestPowershellExecutor : PowershellExecutor
private class TestPowerShellExecutor : PowerShellExecutor
{
public TestPowershellExecutor(MockFixture fixture)
public TestPowerShellExecutor(MockFixture fixture)
: base(fixture.Dependencies, fixture.Parameters)
{
}

public new string ExecutablePath
{
get
{
return base.ExecutablePath;
}
}

public new string ExecutableDirectory
{
get
{
return base.ExecutableDirectory;
}
}

public new Task InitializeAsync(EventContext telemetryContext, CancellationToken cancellationToken)
{
return base.InitializeAsync(telemetryContext, cancellationToken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -327,9 +327,9 @@ public void ScriptExecutorMovesTheLogFilesToCorrectDirectory_Win(PlatformID plat
this.mockFixture.File.Setup(fe => fe.Move(It.IsAny<string>(), It.IsAny<string>(), true))
.Callback<string, string, bool>((sourcePath, destinitionPath, overwrite) =>
{
destinitionPathCorrect = Regex.IsMatch(
destinitionPath,
$"{logsDir}.{this.mockFixture.Parameters["ToolName"].ToString().ToLower()}.{this.mockFixture.Parameters["Scenario"].ToString().ToLower()}");
destinitionPathCorrect = destinitionPath.StartsWith(this.mockFixture.Combine(
logsDir,
this.mockFixture.Parameters["ToolName"].ToString().ToLower()));

sourcePathCorrect = Regex.IsMatch(
sourcePath,
Expand Down Expand Up @@ -363,9 +363,9 @@ public void ScriptExecutorMovesTheLogFilesToCorrectDirectory_Unix(PlatformID pla
this.mockFixture.File.Setup(fe => fe.Move(It.IsAny<string>(), It.IsAny<string>(), true))
.Callback<string, string, bool>((sourcePath, destinitionPath, overwrite) =>
{
destinitionPathCorrect = Regex.IsMatch(
destinitionPath,
$"{logsDir}.{this.mockFixture.Parameters["ToolName"].ToString().ToLower()}.{this.mockFixture.Parameters["Scenario"].ToString().ToLower()}");
destinitionPathCorrect = destinitionPath.StartsWith(this.mockFixture.Combine(
logsDir,
this.mockFixture.Parameters["ToolName"].ToString().ToLower()));

sourcePathCorrect = Regex.IsMatch(
sourcePath,
Expand All @@ -387,6 +387,22 @@ public TestScriptExecutor(MockFixture fixture)
{
}

public new string ExecutablePath
{
get
{
return base.ExecutablePath;
}
}

public new string ExecutableDirectory
{
get
{
return base.ExecutableDirectory;
}
}

public new Task InitializeAsync(EventContext telemetryContext, CancellationToken cancellationToken)
{
return base.InitializeAsync(telemetryContext, cancellationToken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ namespace VirtualClient.Actions
{
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
Expand All @@ -15,26 +16,49 @@ namespace VirtualClient.Actions
/// <summary>
/// The Generic Script executor for Powershell
/// </summary>
public class PowershellExecutor : ScriptExecutor
public class PowerShellExecutor : ScriptExecutor
{
private const string PowerShellExecutableName = "powershell";
private const string PowerShell7ExecutableName = "pwsh";

/// <summary>
/// Constructor for <see cref="PowershellExecutor"/>
/// Constructor for <see cref="PowerShellExecutor"/>
/// </summary>
/// <param name="dependencies">Provides required dependencies to the component.</param>
/// <param name="parameters">Parameters defined in the profile or supplied on the command line.</param>
public PowershellExecutor(IServiceCollection dependencies, IDictionary<string, IConvertible> parameters)
public PowerShellExecutor(IServiceCollection dependencies, IDictionary<string, IConvertible> parameters)
: base(dependencies, parameters)
{
this.ApplyBackwardsCompatibility();
}

/// <summary>
/// The name (or path) of the PowerShell executable to use (e.g. pwsh, PowerShell.exe)
/// </summary>
public string Executable
{
get
{
return this.Parameters.GetValue<string>(nameof(this.Executable), PowerShellExecutor.PowerShellExecutableName);
}
}

/// <summary>
/// The parameter specifies whether to use pwsh, by default it is false
/// </summary>
public bool UsePwsh
private bool UsePwsh
{
get
{
return this.Parameters.GetValue<bool>(nameof(this.UsePwsh), false);
// TODO:
// Remove this property entirely.
//
// This is an indirect way to get to the name of the PowerShell executable (pwsh vs. PowerShell.exe).
// There is no reason to do so indirectly. There are additional benefits to allowing the user to simply
// specify the executable name:
// 1) The usage is just as easy and the outcome is the same.
// 2) A full path to a specific version of PowerShell can be supplied (i.e. flexibility for other use cases).
throw new NotSupportedException("Design Correction. The PowerShell executable name or path should be specified explicitly (e.g. pwsh, PowerShell.exe).");
}
}

Expand All @@ -45,26 +69,45 @@ protected override async Task ExecuteAsync(EventContext telemetryContext, Cancel
{
using (BackgroundOperations profiling = BackgroundOperations.BeginProfiling(this, cancellationToken))
{
string command = this.UsePwsh ? "pwsh" : "powershell";
string commandArguments = SensitiveData.ObscureSecrets(
$"-ExecutionPolicy Bypass -NoProfile -NonInteractive -WindowStyle Hidden -Command \"cd '{this.ExecutableDirectory}';{this.ExecutablePath} {this.CommandLine}\"");
string command = this.Executable;

// Handle nested quotation mark parsing pitfalls.
string commandLine = this.CommandLine.Replace("\"", "\\\"");
string executablePath = this.ExecutablePath.Replace("\"", "\\\"");
string commandArguments = $"-ExecutionPolicy Bypass -NoProfile -NonInteractive -WindowStyle Hidden -Command \"cd '{this.ExecutableDirectory}';{executablePath} {commandLine}\"";

telemetryContext
.AddContext(nameof(command), command)
.AddContext(nameof(commandArguments), commandArguments);
.AddContext(nameof(commandArguments), SensitiveData.ObscureSecrets(commandArguments));

using (IProcessProxy process = await this.ExecuteCommandAsync(command, commandArguments, this.ExecutableDirectory, telemetryContext, cancellationToken, this.RunElevated))
{
if (!cancellationToken.IsCancellationRequested)
{
await this.LogProcessDetailsAsync(process, telemetryContext, this.ToolName);
process.ThrowIfWorkloadFailed();

await this.CaptureMetricsAsync(process, telemetryContext, cancellationToken);
await this.CaptureLogsAsync(cancellationToken);
try
{
await this.LogProcessDetailsAsync(process, telemetryContext, this.ToolName);
process.ThrowIfWorkloadFailed();
}
finally
{
await this.CaptureMetricsAsync(process, telemetryContext, cancellationToken);
await this.CaptureLogsAsync(cancellationToken);
}
}
}
}
}

private void ApplyBackwardsCompatibility()
{
if (this.Parameters.TryGetValue(nameof(this.UsePwsh), out IConvertible usePwsh))
{
bool usePowerShell7 = usePwsh.ToBoolean(CultureInfo.InvariantCulture);
this.Parameters[nameof(this.Executable)] = usePowerShell7
? PowerShellExecutor.PowerShell7ExecutableName
: PowerShellExecutor.PowerShellExecutableName;
}
}
}
}
Loading
Loading