Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Execute Action - Testing #742

7 changes: 7 additions & 0 deletions dotnet-monitor.sln
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Diagnostics.Monit
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Diagnostics.Monitoring.Tool.UnitTests", "src\Tests\Microsoft.Diagnostics.Monitoring.Tool.UnitTests\Microsoft.Diagnostics.Monitoring.Tool.UnitTests.csproj", "{0DBE362D-82F1-4740-AE6A-40C1A82EDCDB}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Diagnostics.Monitoring.ExecuteActionApp", "src\Tests\Microsoft.Diagnostics.Monitoring.ExecuteActionApp\Microsoft.Diagnostics.Monitoring.ExecuteActionApp.csproj", "{A5A0CAAB-C200-44D2-BC93-8445C6E748AD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -92,6 +94,10 @@ Global
{0DBE362D-82F1-4740-AE6A-40C1A82EDCDB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0DBE362D-82F1-4740-AE6A-40C1A82EDCDB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0DBE362D-82F1-4740-AE6A-40C1A82EDCDB}.Release|Any CPU.Build.0 = Release|Any CPU
{A5A0CAAB-C200-44D2-BC93-8445C6E748AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A5A0CAAB-C200-44D2-BC93-8445C6E748AD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A5A0CAAB-C200-44D2-BC93-8445C6E748AD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A5A0CAAB-C200-44D2-BC93-8445C6E748AD}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -112,6 +118,7 @@ Global
{3AD0A40B-C569-4712-9764-7A788B9CD811} = {C7568468-1C79-4944-8136-18812A7F9EA7}
{173F959B-231B-45D1-8328-9460D4C5BC71} = {19FAB78C-3351-4911-8F0C-8C6056401740}
{0DBE362D-82F1-4740-AE6A-40C1A82EDCDB} = {C7568468-1C79-4944-8136-18812A7F9EA7}
{A5A0CAAB-C200-44D2-BC93-8445C6E748AD} = {C7568468-1C79-4944-8136-18812A7F9EA7}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {46465737-C938-44FC-BE1A-4CE139EBB5E0}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>netcoreapp3.1</TargetFrameworks>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Microsoft.Diagnostics.Monitoring.TestCommon\Microsoft.Diagnostics.Monitoring.TestCommon.csproj" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.IO;
using System.Linq;
using System.Threading;
using Xunit;

namespace Microsoft.Diagnostics.Monitoring.ExecuteActionApp
{
internal class Program
{
public static int Main(string[] args)
{
string testType = args[0];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider some argument validation to make it easier to figure out failures

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What did you have in mind? In theory, since for each test we're manually setting the args, shouldn't we know that they'll all be valid?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example:

  • Maybe the default case of the switch should throw an exception stating that the test type is not one of the known ones.
  • Maybe extract the remaining args into a new array that the test types can look instead of looking at the original args parameter. This allows us to change the ordering of the arguments passed on the command line without having to rewrite all of the test cases.
  • Each test type should valid that its args length is exactly what it should expect and throw an exception if it is different.


string[] testArgs = args.Skip(1).ToArray();

switch (testType)
{
case "ZeroExitCode":
Assert.Equal(0, testArgs.Length);
return 0;

case "NonzeroExitCode":
Assert.Equal(0, testArgs.Length);
return 1;

case "Sleep":
Assert.Equal(1, testArgs.Length);
string delayArg = testArgs[0];
int delay = int.Parse(delayArg);
Thread.Sleep(delay);
return 0;

case "TextFileOutput":
Assert.Equal(2, testArgs.Length);
string pathArg = testArgs[0];
string contentsArg = testArgs[1];
File.WriteAllText(pathArg, contentsArg);
return 0;

default:
throw new ArgumentException($"Unknown test type {testType}.");
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options.Actions;
using System.Threading.Tasks;
using Xunit;
using Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Actions;
using System.Reflection;
using Microsoft.Diagnostics.Monitoring.TestCommon;
using System.Threading;
using System;
using System.IO;
using System.Diagnostics;
using Microsoft.Diagnostics.Tools.Monitor;

namespace Microsoft.Diagnostics.Monitoring.Tool.UnitTests
{
public sealed class ExecuteActionTests
jander-msft marked this conversation as resolved.
Show resolved Hide resolved
{
private const int TokenTimeoutMs = 10000;
private const int DelayMs = 1000;

[Fact]
public async Task ExecuteAction_ZeroExitCode()
{
ExecuteAction action = new();

ExecuteOptions options = new();

options.Path = DotNetHost.HostExePath;
options.Arguments = GenerateArgumentsString(new string[] { "ZeroExitCode" });

using CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(TokenTimeoutMs);

CollectionRuleActionResult result = await action.ExecuteAsync(options, null, cancellationTokenSource.Token);

ValidateActionResult(result, "0");
}

[Fact]
public async Task ExecuteAction_NonzeroExitCode()
{
ExecuteAction action = new();

ExecuteOptions options = new();

options.Path = DotNetHost.HostExePath;
options.Arguments = GenerateArgumentsString(new string[] { "NonzeroExitCode" });

using CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(TokenTimeoutMs);

InvalidOperationException invalidOperationException = await Assert.ThrowsAsync<InvalidOperationException>(
() => action.ExecuteAsync(options, null, cancellationTokenSource.Token));

Assert.Contains(string.Format(Strings.ErrorMessage_NonzeroExitCode, "1"), invalidOperationException.Message);
}

[Fact]
public async Task ExecuteAction_TokenCancellation()
{
ExecuteAction action = new();

ExecuteOptions options = new();

options.Path = DotNetHost.HostExePath;
options.Arguments = GenerateArgumentsString(new string[] { "Sleep", (TokenTimeoutMs + DelayMs).ToString() }); ;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might work most of the time. If it's not stable enough, maybe we can change this to write out a file (like the TextFileOutput scenario) and then sleep for some large amount of time. Back in the test, we start the process, wait for the file to appear, and then immediately cancel the token. This would provide more determinism so that if the host happens to be really slow that process startup doesn't exceed the token timeout.


using CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(TokenTimeoutMs);

TaskCanceledException taskCanceledException = await Assert.ThrowsAsync<TaskCanceledException>(
() => action.ExecuteAsync(options, null, cancellationTokenSource.Token));
}

[Fact]
public async Task ExecuteAction_TextFileOutput()
{
ExecuteAction action = new();

ExecuteOptions options = new();

DirectoryInfo outputDirectory = null;

try
{
outputDirectory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), "ExecuteAction", Guid.NewGuid().ToString()));
string textFileOutputPath = Path.Combine(outputDirectory.FullName, "file.txt");

const string testMessage = "TestMessage";

options.Path = DotNetHost.HostExePath;
options.Arguments = GenerateArgumentsString(new string[] { "TextFileOutput", textFileOutputPath, testMessage });

using CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(TokenTimeoutMs);

CollectionRuleActionResult result = await action.ExecuteAsync(options, null, cancellationTokenSource.Token);

ValidateActionResult(result, "0");

Assert.Equal(testMessage, File.ReadAllText(textFileOutputPath));
}
finally
{
try
{
outputDirectory?.Delete(recursive: true);
}
catch
{
}
}
}

[Fact]
public async Task ExecuteAction_InvalidPath()
{
ExecuteAction action = new();

ExecuteOptions options = new();

string uniquePathName = Guid.NewGuid().ToString();

options.Path = uniquePathName;
options.Arguments = GenerateArgumentsString(Array.Empty<string>());

using CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(TokenTimeoutMs);

FileNotFoundException fileNotFoundException = await Assert.ThrowsAsync<FileNotFoundException>(
() => action.ExecuteAsync(options, null, cancellationTokenSource.Token));

Assert.Equal(string.Format(Strings.ErrorMessage_FileNotFound, uniquePathName), fileNotFoundException.Message);
}

[Fact]
public async Task ExecuteAction_IgnoreExitCode()
{
ExecuteAction action = new();

ExecuteOptions options = new();

options.Path = DotNetHost.HostExePath;
options.Arguments = GenerateArgumentsString(new string[] { "NonzeroExitCode" });
options.IgnoreExitCode = true;

using CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(TokenTimeoutMs);

CollectionRuleActionResult result = await action.ExecuteAsync(options, null, cancellationTokenSource.Token);

ValidateActionResult(result, "1");
}

private static string GenerateArgumentsString(string[] additionalArgs)
{
Assembly currAssembly = Assembly.GetExecutingAssembly();

return AssemblyHelper.GetAssemblyArtifactBinPath(currAssembly, "Microsoft.Diagnostics.Monitoring.ExecuteActionApp", TargetFrameworkMoniker.NetCoreApp31)
kkeirstead marked this conversation as resolved.
Show resolved Hide resolved
+ ' ' + string.Join(' ', additionalArgs);
}

private static void ValidateActionResult(CollectionRuleActionResult result, string expectedExitCode)
{
string actualExitCode;

Assert.NotNull(result.OutputValues);
Assert.True(result.OutputValues.TryGetValue("ExitCode", out actualExitCode));
Assert.Equal(expectedExitCode, actualExitCode);
}
}
}
1 change: 1 addition & 0 deletions src/Tools/dotnet-monitor/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@
</data>
<data name="ErrorMessage_UnableToStartProcess" xml:space="preserve">
<value>Unable to start: {0} {1}</value>
<comment>{0} = FileName, {1} = Process's Arguments</comment>
</data>
<data name="ErrorMessage_UnhandledConnectionMode" xml:space="preserve">
<value>Unhandled connection mode: {0}</value>
Expand Down