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

[temporary one] diagnostics for 2160 #2326

Closed
wants to merge 12 commits into from
Closed
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
57 changes: 49 additions & 8 deletions src/System.CommandLine.Suggest.Tests/DotnetSuggestEndToEndTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@
using static System.Environment;
using Process = System.CommandLine.Tests.Utility.Process;

namespace System.CommandLine.Suggest.Tests
namespace System.CommandLine.Suggest.Tests
{
public class DotnetSuggestEndToEndTests : IDisposable
{
private readonly ITestOutputHelper _output;
private readonly FileInfo _endToEndTestApp;
private readonly FileInfo _waitAndFailTestApp;
private readonly FileInfo _dotnetSuggest;
private readonly (string, string)[] _environmentVariables;
private readonly DirectoryInfo _dotnetHostDir = DotnetMuxer.Path.Directory;
Expand All @@ -25,14 +26,15 @@ public DotnetSuggestEndToEndTests(ITestOutputHelper output)
{
_output = output;

// delete sentinel files for EndToEndTestApp in order to trigger registration when it's run
// delete sentinel files for TestApps in order to trigger registration when it's run
var sentinelsDir = new DirectoryInfo(Path.Combine(Path.GetTempPath(), "system-commandline-sentinel-files"));

if (sentinelsDir.Exists)
{
var sentinels = sentinelsDir.GetFiles("*EndToEndTestApp*");
var sentinelsEndToEnd = sentinelsDir.GetFiles("*EndToEndTestApp*");
var sentinelsWaitAndFail = sentinelsDir.GetFiles("*WaitAndFailTestApp*");

foreach (var sentinel in sentinels)
foreach (var sentinel in sentinelsEndToEnd.Concat(sentinelsWaitAndFail))
{
sentinel.Delete();
}
Expand All @@ -43,12 +45,16 @@ public DotnetSuggestEndToEndTests(ITestOutputHelper output)
"TestAssets");

_endToEndTestApp = new DirectoryInfo(currentDirectory)
.GetFiles("EndToEndTestApp".ExecutableName())
.SingleOrDefault();
.GetFiles("EndToEndTestApp".ExecutableName())
.SingleOrDefault();

_waitAndFailTestApp = new DirectoryInfo(currentDirectory)
.GetFiles("WaitAndFailTestApp".ExecutableName())
.SingleOrDefault();

_dotnetSuggest = new DirectoryInfo(currentDirectory)
.GetFiles("dotnet-suggest".ExecutableName())
.SingleOrDefault();
.GetFiles("dotnet-suggest".ExecutableName())
.SingleOrDefault();

PrepareTestHomeDirectoryToAvoidPolluteBuildMachineHome();

Expand Down Expand Up @@ -156,5 +162,40 @@ public void Dotnet_suggest_provides_suggestions_for_app_with_only_commandname()
.Should()
.Be($"--apple{NewLine}--banana{NewLine}--cherry{NewLine}--durian{NewLine}--help{NewLine}--version{NewLine}-?{NewLine}-h{NewLine}/?{NewLine}/h{NewLine}");
}

[ReleaseBuildOnlyFact]
public void Dotnet_suggest_fails_to_provide_suggestions_because_app_faulted()
{
// run "dotnet-suggest register" in explicit way
Process.RunToCompletion(
_dotnetSuggest.FullName,
$"register --command-path \"{_waitAndFailTestApp.FullName}\"",
stdOut: s => _output.WriteLine(s),
stdErr: s => _output.WriteLine(s),
environmentVariables: _environmentVariables).Should().Be(0);

var stdOut = new StringBuilder();
var stdErr = new StringBuilder();

var commandLineToComplete = "a";

Process.RunToCompletion(
_dotnetSuggest.FullName,
$"get -e \"{_waitAndFailTestApp.FullName}\" --position {commandLineToComplete.Length} -- \"{commandLineToComplete}\"",
stdOut: value => stdOut.AppendLine(value),
stdErr: value => stdErr.AppendLine(value),
environmentVariables: _environmentVariables);

_output.WriteLine($"stdOut:{NewLine}{stdOut}{NewLine}");
_output.WriteLine($"stdErr:{NewLine}{stdErr}{NewLine}");

stdErr.ToString()
.Should()
.BeEmpty();

stdOut.ToString()
.Should()
.BeEmpty();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>$(TargetFrameworkForNETSDK)</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>

</Project>
140 changes: 140 additions & 0 deletions src/System.CommandLine.Suggest.Tests/SuggestionStoreTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.CommandLine.Tests.Utility;
using System.IO;
using System.Linq;
using FluentAssertions;
using Xunit.Abstractions;
using static System.Environment;

namespace System.CommandLine.Suggest.Tests
{
public class SuggestionStoreTests : IDisposable
{
protected readonly ITestOutputHelper _output;
protected readonly FileInfo _endToEndTestApp;
protected readonly FileInfo _waitAndFailTestApp;
protected readonly FileInfo _dotnetSuggest;
protected readonly (string, string)[] _environmentVariables;
private readonly DirectoryInfo _dotnetHostDir = DotnetMuxer.Path.Directory;
private static string _testRoot;

public SuggestionStoreTests(ITestOutputHelper output)
{
_output = output;

// delete sentinel files for TestApps in order to trigger registration when it's run
var sentinelsDir = new DirectoryInfo(Path.Combine(Path.GetTempPath(), "system-commandline-sentinel-files"));

if (sentinelsDir.Exists)
{
var sentinels = sentinelsDir
.EnumerateFiles()
.Where(f => f.Name.Contains("EndToEndTestApp") || f.Name.Contains("WaitAndFailTestApp"));

foreach (var sentinel in sentinels)
{
sentinel.Delete();
}
}

var currentDirectory = Path.Combine(
Directory.GetCurrentDirectory(),
"TestAssets");

_endToEndTestApp = new DirectoryInfo(currentDirectory)
.GetFiles("EndToEndTestApp".ExecutableName())
.SingleOrDefault();

_waitAndFailTestApp = new DirectoryInfo(currentDirectory)
.GetFiles("WaitAndFailTestApp".ExecutableName())
.SingleOrDefault();

_dotnetSuggest = new DirectoryInfo(currentDirectory)
.GetFiles("dotnet-suggest".ExecutableName())
.SingleOrDefault();

PrepareTestHomeDirectoryToAvoidPolluteBuildMachineHome();

_environmentVariables = new[] {
("DOTNET_ROOT", _dotnetHostDir.FullName),
("INTERNAL_TEST_DOTNET_SUGGEST_HOME", _testRoot)};
}

public void Dispose()
{
if (_testRoot != null && Directory.Exists(_testRoot))
{
Directory.Delete(_testRoot, recursive: true);
}
}

private static void PrepareTestHomeDirectoryToAvoidPolluteBuildMachineHome()
{
_testRoot = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
Directory.CreateDirectory(_testRoot);
}

[ReleaseBuildOnlyFact]
public void GetCompletions_obtains_suggestions_successfully()
{
// run "dotnet-suggest register" in explicit way
Process.RunToCompletion(
_dotnetSuggest.FullName,
$"register --command-path \"{_endToEndTestApp.FullName}\"",
stdOut: s => _output.WriteLine(s),
stdErr: s => _output.WriteLine(s),
environmentVariables: _environmentVariables).Should().Be(0);

var store = new SuggestionStore();
var completions = store.GetCompletions(_endToEndTestApp.FullName, "[suggest:1] \"a\"", TimeSpan.FromSeconds(1));
completions.Should().Be($"--apple{NewLine}--banana{NewLine}--durian{NewLine}");
}

[ReleaseBuildOnlyFact]
public void GetCompletions_fails_to_obtain_suggestions_because_app_takes_too_long()
{
var store = new SuggestionStore();
var appHangingTimeSpanArgument = TimeSpan.FromMilliseconds(2000).ToString();
var completions = store
.GetCompletions(_waitAndFailTestApp.FullName, appHangingTimeSpanArgument, TimeSpan.FromMilliseconds(1000));
completions.Should().BeEmpty();
}

[ReleaseBuildOnlyFact]
public void GetCompletions_fails_to_obtain_suggestions_because_app_exited_with_nonzero_code()
{
var store = new SuggestionStore();
var appHangingTimeSpanArgument = TimeSpan.FromMilliseconds(0).ToString();
var completions = store
.GetCompletions(_waitAndFailTestApp.FullName, appHangingTimeSpanArgument, TimeSpan.FromMilliseconds(1000));
completions.Should().BeEmpty();
}

[ReleaseBuildOnlyFact]
public void GetCompletions_throws_when_exeFileName_is_null()
{
var store = new SuggestionStore();
var act = () => store.GetCompletions(null, "[suggest:1] \"a\"", TimeSpan.FromSeconds(1));
act.Should().Throw<ArgumentException>();
}

[ReleaseBuildOnlyFact]
public void GetCompletions_throws_when_exeFileName_is_empty()
{
var store = new SuggestionStore();
var act = () => store.GetCompletions(" ", "[suggest:1] \"a\"", TimeSpan.FromSeconds(1));
act.Should().Throw<ArgumentException>();
}

[ReleaseBuildOnlyFact]
public void GetCompletions_throws_when_exeFile_does_not_exist()
{
var store = new SuggestionStore();
var act = () => store
.GetCompletions("thisExecutableDoesNotExist.exe", "[suggest:1] \"a\"", TimeSpan.FromSeconds(1));
act.Should().Throw<ArgumentException>();
}
}
}
25 changes: 25 additions & 0 deletions src/System.CommandLine.Suggest.Tests/WaitAndFailTestApp/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System;
using System.Threading.Tasks;

namespace WaitAndFailTestApp;

public class Program
{
private static TimeSpan defaultWait = TimeSpan.FromMilliseconds(3000);

//we should not be able to receive any suggestion from this test app,
//so we are not constructing it using CliConfiguration

static async Task Main(string[] args)
{
var waitPeriod = args.Length > 0 && int.TryParse(args[0], out var millisecondsToWaitParsed)
? TimeSpan.FromMilliseconds(millisecondsToWaitParsed)
: defaultWait;

await Task.Delay(waitPeriod);
Environment.ExitCode = 1;

Console.WriteLine("this 'suggestion' is provided too late and/or with invalid app exit code");
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">

<ItemGroup>
<ProjectReference Include="..\..\System.CommandLine\System.CommandLine.csproj" />
</ItemGroup>

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>

</Project>
22 changes: 21 additions & 1 deletion src/System.CommandLine.Suggest.Tests/dotnet-suggest.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,25 @@

<PropertyGroup>
<TargetFramework>$(TargetFrameworkForNETSDK)</TargetFramework>
<DefaultExcludesInProjectFolder>$(DefaultExcludesInProjectFolder);EndToEndTestApp\**</DefaultExcludesInProjectFolder>
<DefaultExcludesInProjectFolder>$(DefaultExcludesInProjectFolder);EndToEndTestApp\**;WaitAndFailTestApp\**</DefaultExcludesInProjectFolder>
</PropertyGroup>

<ItemGroup>
<Compile Remove="TestResults\**" />
<EmbeddedResource Remove="TestResults\**" />
<None Remove="TestResults\**" />

<Compile Remove="EndToEndTestApp/**" />
<Content Remove="EndToEndTestApp/**" />
<EmbeddedResource Remove="EndToEndTestApp/**" />
<None Remove="EndToEndTestApp/**" />

<Compile Remove="WaitAndFailTestApp/**" />
<Content Remove="WaitAndFailTestApp/**" />
<EmbeddedResource Remove="WaitAndFailTestApp/**" />
<None Remove="WaitAndFailTestApp/**" />
</ItemGroup>

<ItemGroup>
<Compile Include="..\System.CommandLine.Suggest\DotnetMuxer.cs" Link="DotnetMuxer.cs" />
</ItemGroup>
Expand Down Expand Up @@ -42,6 +58,10 @@
<MSBuild BuildInParallel="False" Projects="EndToEndTestApp/EndToEndTestApp.csproj" Targets="Restore" Properties="UseAppHost=true;SelfContained=false;RuntimeIdentifier=$(Rid);ForceRestoreToEvaluateSeparately=1;Configuration=Release" />

<MSBuild BuildInParallel="False" Projects="EndToEndTestApp/EndToEndTestApp.csproj" Targets="Build;Publish" Properties="UseAppHost=true;SelfContained=false;RuntimeIdentifier=$(Rid);PublishDir=$(TestAssetsPath);Configuration=Release" />

<MSBuild BuildInParallel="False" Projects="WaitAndFailTestApp/WaitAndFailTestApp.csproj" Targets="Restore" Properties="UseAppHost=true;SelfContained=false;RuntimeIdentifier=$(Rid);ForceRestoreToEvaluateSeparately=1;Configuration=Release" />

<MSBuild BuildInParallel="False" Projects="WaitAndFailTestApp/WaitAndFailTestApp.csproj" Targets="Build;Publish" Properties="UseAppHost=true;SelfContained=false;RuntimeIdentifier=$(Rid);PublishDir=$(TestAssetsPath);Configuration=Release" />
</Target>

</Project>
25 changes: 19 additions & 6 deletions src/System.CommandLine.Suggest/SuggestionStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading.Tasks;

namespace System.CommandLine.Suggest
Expand Down Expand Up @@ -37,19 +38,31 @@ public string GetCompletions(string exeFileName, string suggestionTargetArgument

using (var process = new Process
{
StartInfo = processStartInfo
StartInfo = processStartInfo,
})
{

process.Start();

Task<string> readToEndTask = process.StandardOutput.ReadToEndAsync();

if (readToEndTask.Wait(timeout))

var stringBuilder = new StringBuilder();
process.OutputDataReceived += (sender, eventArgs) =>
{
if (eventArgs.Data != null)
{
stringBuilder.AppendLine(eventArgs.Data);
}
};
process.BeginOutputReadLine();
if (process.WaitForExit(timeout) && process.ExitCode == 0)
{
result = readToEndTask.Result;
process.CancelOutputRead();
result = stringBuilder.ToString();
}
else
{
#if DEBUG
Program.LogDebug($"Killing the process after a timeout. Process exit code: {process.ExitCode}");
#endif
process.Kill();
}
}
Expand Down