diff --git a/src/System.CommandLine.Suggest.Tests/DotnetSuggestEndToEndTests.cs b/src/System.CommandLine.Suggest.Tests/DotnetSuggestEndToEndTests.cs index 7e08fc8d65..27c4e9f672 100644 --- a/src/System.CommandLine.Suggest.Tests/DotnetSuggestEndToEndTests.cs +++ b/src/System.CommandLine.Suggest.Tests/DotnetSuggestEndToEndTests.cs @@ -3,8 +3,6 @@ using FluentAssertions; using System.CommandLine.Tests.Utility; -using System.IO; -using System.Linq; using System.Text; using Xunit.Abstractions; using static System.Environment; @@ -12,75 +10,24 @@ 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() @@ -92,11 +39,11 @@ 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(); @@ -104,14 +51,14 @@ public void Dotnet_suggest_provides_suggestions_for_app() 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() @@ -127,11 +74,11 @@ 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(); @@ -139,14 +86,14 @@ public void Dotnet_suggest_provides_suggestions_for_app_with_only_commandname() 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() @@ -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(); + } } } diff --git a/src/System.CommandLine.Suggest.Tests/SuggestionStoreTests.cs b/src/System.CommandLine.Suggest.Tests/SuggestionStoreTests.cs new file mode 100644 index 0000000000..ba3665bce2 --- /dev/null +++ b/src/System.CommandLine.Suggest.Tests/SuggestionStoreTests.cs @@ -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(); + } + } +} diff --git a/src/System.CommandLine.Suggest.Tests/TestsWithTestApps.cs b/src/System.CommandLine.Suggest.Tests/TestsWithTestApps.cs new file mode 100644 index 0000000000..485c9160a2 --- /dev/null +++ b/src/System.CommandLine.Suggest.Tests/TestsWithTestApps.cs @@ -0,0 +1,74 @@ +using System.IO; +using System.Linq; +using Xunit; +using Xunit.Abstractions; + +namespace System.CommandLine.Suggest.Tests; + +[Collection("TestsWithTestApps")] +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); + } +} \ No newline at end of file diff --git a/src/System.CommandLine.Suggest.Tests/WaitAndFailTestApp/Program.cs b/src/System.CommandLine.Suggest.Tests/WaitAndFailTestApp/Program.cs new file mode 100644 index 0000000000..d1d22b38ab --- /dev/null +++ b/src/System.CommandLine.Suggest.Tests/WaitAndFailTestApp/Program.cs @@ -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"); + } +} + diff --git a/src/System.CommandLine.Suggest.Tests/WaitAndFailTestApp/WaitAndFailTestApp.csproj b/src/System.CommandLine.Suggest.Tests/WaitAndFailTestApp/WaitAndFailTestApp.csproj new file mode 100644 index 0000000000..186b884e98 --- /dev/null +++ b/src/System.CommandLine.Suggest.Tests/WaitAndFailTestApp/WaitAndFailTestApp.csproj @@ -0,0 +1,12 @@ + + + + + + + + Exe + net7.0 + + + diff --git a/src/System.CommandLine.Suggest.Tests/dotnet-suggest.Tests.csproj b/src/System.CommandLine.Suggest.Tests/dotnet-suggest.Tests.csproj index f1711a10e1..2869f2abf6 100644 --- a/src/System.CommandLine.Suggest.Tests/dotnet-suggest.Tests.csproj +++ b/src/System.CommandLine.Suggest.Tests/dotnet-suggest.Tests.csproj @@ -13,6 +13,11 @@ + + + + + @@ -67,6 +72,12 @@ + + + + + + diff --git a/src/System.CommandLine.Suggest/SuggestionStore.cs b/src/System.CommandLine.Suggest/SuggestionStore.cs index ac93972eb1..d77e547f5a 100644 --- a/src/System.CommandLine.Suggest/SuggestionStore.cs +++ b/src/System.CommandLine.Suggest/SuggestionStore.cs @@ -4,6 +4,7 @@ using System.ComponentModel; using System.Diagnostics; using System.IO; +using System.Text; using System.Threading.Tasks; namespace System.CommandLine.Suggest @@ -40,16 +41,28 @@ public string GetCompletions(string exeFileName, string suggestionTargetArgument StartInfo = processStartInfo }) { + process.Start(); - - Task 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(); } }