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

SuggestionStore::GetCompletions to check exit code of invoked applica… #2160

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 3 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
142 changes: 62 additions & 80 deletions src/System.CommandLine.Suggest.Tests/DotnetSuggestEndToEndTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,84 +3,31 @@

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

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

public DotnetSuggestEndToEndTests(ITestOutputHelper output)
public DotnetSuggestEndToEndTests(ITestOutputHelper output) : base(output)
{
_output = output;

// delete sentinel files for EndToEndTestApp 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*");

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

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

_endToEndTestApp = new DirectoryInfo(currentDirectory)
.GetFiles("EndToEndTestApp".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 Test_app_supplies_suggestions()
{
var stdOut = new StringBuilder();


Output.WriteLine($"_endToEndTestApp.FullName: {EndToEndTestApp.FullName}");

Process.RunToCompletion(
_endToEndTestApp.FullName,
EndToEndTestApp.FullName,
"[suggest:1] \"a\"",
stdOut: value => stdOut.AppendLine(value),
environmentVariables: _environmentVariables);
environmentVariables: EnvironmentVariables);

stdOut.ToString()
.Should()
Expand All @@ -92,26 +39,26 @@ public void Dotnet_suggest_provides_suggestions_for_app()
{
// 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);
DotnetSuggest.FullName,
$"register --command-path \"{EndToEndTestApp.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 \"{_endToEndTestApp.FullName}\" --position {commandLineToComplete.Length} -- \"{commandLineToComplete}\"",
DotnetSuggest.FullName,
$"get -e \"{EndToEndTestApp.FullName}\" --position {commandLineToComplete.Length} -- \"{commandLineToComplete}\"",
stdOut: value => stdOut.AppendLine(value),
stdErr: value => stdErr.AppendLine(value),
environmentVariables: _environmentVariables);
environmentVariables: EnvironmentVariables);

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

stdErr.ToString()
.Should()
Expand All @@ -127,26 +74,26 @@ public void Dotnet_suggest_provides_suggestions_for_app_with_only_commandname()
{
// 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);
DotnetSuggest.FullName,
$"register --command-path \"{EndToEndTestApp.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 \"{_endToEndTestApp.FullName}\" --position {commandLineToComplete.Length} -- \"{commandLineToComplete}\"",
DotnetSuggest.FullName,
$"get -e \"{EndToEndTestApp.FullName}\" --position {commandLineToComplete.Length} -- \"{commandLineToComplete}\"",
stdOut: value => stdOut.AppendLine(value),
stdErr: value => stdErr.AppendLine(value),
environmentVariables: _environmentVariables);
environmentVariables: EnvironmentVariables);

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

stdErr.ToString()
.Should()
Expand All @@ -156,5 +103,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();
}
}
}
45 changes: 45 additions & 0 deletions src/System.CommandLine.Suggest.Tests/SuggestionStoreTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// 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 FluentAssertions;
using Xunit.Abstractions;
using static System.Environment;

namespace System.CommandLine.Suggest.Tests
{
public class SuggestionStoreTests : TestsWithTestApps
{
public SuggestionStoreTests(ITestOutputHelper output) : base(output)
{
}

[ReleaseBuildOnlyFact]
public void GetCompletions_obtains_suggestions_successfully()
{
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();
}
}
}
74 changes: 74 additions & 0 deletions src/System.CommandLine.Suggest.Tests/TestsWithTestApps.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
using System.IO;
using System.Linq;
using Xunit;
using Xunit.Abstractions;

namespace System.CommandLine.Suggest.Tests;

[Collection("TestsWithTestApps")]
Copy link
Author

Choose a reason for hiding this comment

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

Note: this attribute is inherited, so both SuggestionStoreTests and DotnetSuggestEndToEndTests will run in the same collection, meaning - not in parallel. This allows the gracefull cleanup of TestsApp files.

public class TestsWithTestApps : 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;

protected TestsWithTestApps(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);
}
}
25 changes: 25 additions & 0 deletions src/System.CommandLine.Suggest.Tests/WaitAndFailTestApp/Program.cs
Copy link
Author

Choose a reason for hiding this comment

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

Not sure whether that solution with the faulted console app was the right way, honestly.

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>net7.0</TargetFramework>
</PropertyGroup>

</Project>
11 changes: 11 additions & 0 deletions src/System.CommandLine.Suggest.Tests/dotnet-suggest.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
<Content Remove="EndToEndTestApp/**" />
<EmbeddedResource Remove="EndToEndTestApp/**" />
<None Remove="EndToEndTestApp/**" />

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

<ItemGroup>
Expand Down Expand Up @@ -67,6 +72,12 @@

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

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

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

</Target>

Expand Down
Loading