From 9279ad3b9c5812c5f36c9878c2e6b4515d3b4471 Mon Sep 17 00:00:00 2001 From: Christian Rondeau Date: Tue, 21 Jul 2015 21:50:32 -0400 Subject: [PATCH] (GH-181) Short prompt + (GH-184) Prompt character --- .../commandline/InteractivePromptSpecs.cs | 203 ++++++++++++++++++ .../runners/GenericRunner.cs | 8 +- .../services/AutomaticUninstallerService.cs | 2 +- .../services/ChocolateyPackageService.cs | 2 +- .../services/PowershellService.cs | 5 +- .../infrastructure/adapters/Console.cs | 5 + .../infrastructure/adapters/IConsole.cs | 28 +-- .../commandline/InteractivePrompt.cs | 43 ++++ .../logging/ChocolateyLoggers.cs | 2 + .../infrastructure/logging/log4net.config.xml | 4 + 10 files changed, 270 insertions(+), 32 deletions(-) diff --git a/src/chocolatey.tests/infrastructure/commandline/InteractivePromptSpecs.cs b/src/chocolatey.tests/infrastructure/commandline/InteractivePromptSpecs.cs index 0b3c4eaf66..29c8fb2e13 100644 --- a/src/chocolatey.tests/infrastructure/commandline/InteractivePromptSpecs.cs +++ b/src/chocolatey.tests/infrastructure/commandline/InteractivePromptSpecs.cs @@ -437,5 +437,208 @@ public void should_error_when_any_choice_not_available_is_given() console.Verify(c => c.ReadLine(), Times.AtLeast(8)); } } + + public class when_prompting_short_with_interactivePrompt_guard_errors : InteractivePromptSpecsBase + { + private Func prompt; + + public override void Because() + { + console.Setup(c => c.ReadLine()).Returns(""); //Enter pressed + prompt = () => InteractivePrompt.prompt_for_confirmation_short(prompt_value, choices); + } + + [Fact] + public void should_error_when_the_choicelist_is_null() + { + choices = null; + bool errored = false; + console.Setup(c => c.ReadLine()).Returns(""); //Enter pressed + try + { + prompt(); + } + catch (Exception) + { + errored = true; + } + + errored.ShouldBeTrue(); + console.Verify(c => c.ReadLine(), Times.Never); + } + + [Fact] + public void should_error_when_the_choicelist_is_empty() + { + choices = new List(); + bool errored = false; + string errorMessage = string.Empty; + console.Setup(c => c.ReadLine()).Returns(""); //Enter pressed + try + { + prompt(); + } + catch (Exception ex) + { + errored = true; + errorMessage = ex.Message; + } + + errored.ShouldBeTrue(); + errorMessage.ShouldContain("No choices passed in."); + console.Verify(c => c.ReadLine(), Times.Never); + } + + [Fact] + public void should_error_when_the_prompt_input_is_null() + { + choices = new List { "bob" }; + prompt_value = null; + bool errored = false; + string errorMessage = string.Empty; + console.Setup(c => c.ReadLine()).Returns(""); //Enter pressed + try + { + prompt(); + } + catch (Exception ex) + { + errored = true; + errorMessage = ex.Message; + } + + errored.ShouldBeTrue(); + errorMessage.ShouldContain("Value for prompt cannot be null."); + console.Verify(c => c.ReadLine(), Times.Never); + } + + [Fact] + public void should_error_when_the_choicelist_contains_empty_values() + { + choices = new List { "bob", "" }; + bool errored = false; + string errorMessage = string.Empty; + console.Setup(c => c.ReadLine()).Returns(""); //Enter pressed + try + { + prompt(); + } + catch (Exception ex) + { + errored = true; + errorMessage = ex.Message; + } + + errored.ShouldBeTrue(); + errorMessage.ShouldContain("Some choices are empty."); + console.Verify(c => c.ReadLine(), Times.Never); + } + + [Fact] + public void should_error_when_the_choicelist_has_multiple_items_with_same_first_letter() + { + choices = new List {"sally", "suzy"}; + bool errored = false; + string errorMessage = string.Empty; + console.Setup(c => c.ReadLine()).Returns(""); //Enter pressed + try + { + prompt(); + } + catch (Exception ex) + { + errored = true; + errorMessage = ex.Message; + } + + errored.ShouldBeTrue(); + errorMessage.ShouldContain("Multiple choices have the same first letter."); + console.Verify(c => c.ReadLine(), Times.Never); + } + } + + public class when_prompting_short_with_interactivePrompt : InteractivePromptSpecsBase + { + private Func prompt; + + public override void Because() + { + prompt = () => InteractivePrompt.prompt_for_confirmation_short(prompt_value, choices); + } + + public override void AfterObservations() + { + base.AfterObservations(); + should_have_called_Console_ReadLine(); + } + + [Fact] + public void should_error_when_no_answer_given() + { + bool errored = false; + + console.Setup(c => c.ReadLine()).Returns(""); //Enter pressed + try + { + prompt(); + } + catch (Exception) + { + errored = true; + } + errored.ShouldBeTrue(); + console.Verify(c => c.ReadLine(), Times.AtLeast(8)); + } + + [Fact] + public void should_return_yes_when_yes_is_given() + { + console.Setup(c => c.ReadLine()).Returns("yes"); + var result = prompt(); + result.ShouldEqual("yes"); + } + + [Fact] + public void should_return_yes_when_y_is_given() + { + console.Setup(c => c.ReadLine()).Returns("y"); + var result = prompt(); + result.ShouldEqual("yes"); + } + + [Fact] + public void should_return_no_choice_when_no_is_given() + { + console.Setup(c => c.ReadLine()).Returns("no"); + var result = prompt(); + result.ShouldEqual("no"); + } + + [Fact] + public void should_return_no_choice_when_n_is_given() + { + console.Setup(c => c.ReadLine()).Returns("n"); + var result = prompt(); + result.ShouldEqual("no"); + } + + [Fact] + public void should_error_when_any_choice_not_available_is_given() + { + bool errored = false; + + console.Setup(c => c.ReadLine()).Returns("yup"); //Enter pressed + try + { + prompt(); + } + catch (Exception) + { + errored = true; + } + errored.ShouldBeTrue(); + console.Verify(c => c.ReadLine(), Times.AtLeast(8)); + } + } } } \ No newline at end of file diff --git a/src/chocolatey/infrastructure.app/runners/GenericRunner.cs b/src/chocolatey/infrastructure.app/runners/GenericRunner.cs index 74ff83d398..6125aefcf6 100644 --- a/src/chocolatey/infrastructure.app/runners/GenericRunner.cs +++ b/src/chocolatey/infrastructure.app/runners/GenericRunner.cs @@ -157,14 +157,14 @@ public void warn_when_admin_needs_elevation(ChocolateyConfiguration config) if (!config.Information.IsProcessElevated && config.Information.IsUserAdministrator) { - var selection = InteractivePrompt.prompt_for_confirmation(@" + var selection = InteractivePrompt.prompt_for_confirmation_short(@" Chocolatey detected you are not running from an elevated command shell (cmd/powershell). You may experience errors - many functions/packages require admin rights. Only advanced users should run choco w/out an elevated shell. When you open the command shell, you should ensure that you do so with ""Run as Administrator"" selected. - Do you want to continue?", new[] { "yes", "no" }, defaultChoice: null, requireAnswer: true); + Do you want to continue?", new[] { "yes", "no" }); if (selection.is_equal_to("no")) { @@ -174,5 +174,5 @@ require admin rights. Only advanced users should run choco w/out an } } -} - +} + diff --git a/src/chocolatey/infrastructure.app/services/AutomaticUninstallerService.cs b/src/chocolatey/infrastructure.app/services/AutomaticUninstallerService.cs index f6a09d5991..68eb93664a 100644 --- a/src/chocolatey/infrastructure.app/services/AutomaticUninstallerService.cs +++ b/src/chocolatey/infrastructure.app/services/AutomaticUninstallerService.cs @@ -142,7 +142,7 @@ public void run(PackageResult packageResult, ChocolateyConfiguration config) var skipUninstaller = true; if (config.PromptForConfirmation) { - var selection = InteractivePrompt.prompt_for_confirmation("Uninstall may not be silent (could not detect). Proceed?", new[] {"yes", "no"}, defaultChoice: null, requireAnswer: true); + var selection = InteractivePrompt.prompt_for_confirmation_short("Uninstall may not be silent (could not detect). Proceed?", new[] {"yes", "no"}); if (selection.is_equal_to("yes")) skipUninstaller = false; } diff --git a/src/chocolatey/infrastructure.app/services/ChocolateyPackageService.cs b/src/chocolatey/infrastructure.app/services/ChocolateyPackageService.cs index de88e159bb..1d395c703f 100644 --- a/src/chocolatey/infrastructure.app/services/ChocolateyPackageService.cs +++ b/src/chocolatey/infrastructure.app/services/ChocolateyPackageService.cs @@ -781,7 +781,7 @@ private void rollback_previous_version(ChocolateyConfiguration config, PackageRe var rollback = true; if (config.PromptForConfirmation) { - var selection = InteractivePrompt.prompt_for_confirmation(" Unsuccessful operation for {0}.{1} Do you want to rollback to previous version (package files only)?".format_with(packageResult.Name, Environment.NewLine), new[] { "yes", "no" }, defaultChoice: null, requireAnswer: true); + var selection = InteractivePrompt.prompt_for_confirmation_short(" Unsuccessful operation for {0}.{1} Do you want to rollback to previous version (package files only)?".format_with(packageResult.Name, Environment.NewLine), new[] { "yes", "no" }); if (selection.is_equal_to("no")) rollback = false; } diff --git a/src/chocolatey/infrastructure.app/services/PowershellService.cs b/src/chocolatey/infrastructure.app/services/PowershellService.cs index 2f9259d100..3a97ecbd7b 100644 --- a/src/chocolatey/infrastructure.app/services/PowershellService.cs +++ b/src/chocolatey/infrastructure.app/services/PowershellService.cs @@ -221,16 +221,17 @@ public bool run_action(ChocolateyConfiguration configuration, PackageResult pack this.Log().Info(ChocolateyLoggers.Important, () => @"Note: To confirm automatically next time, use '-y' or consider setting 'allowGlobalConfirmation'. Run 'choco feature -h' for more details."); - var selection = InteractivePrompt.prompt_for_confirmation(@"Do you want to run the script?", new[] {"yes", "no", "print"}, defaultChoice: null, requireAnswer: true); + var selection = InteractivePrompt.prompt_for_confirmation_short(@"Do you want to run the script?", new[] {"yes", "no", "print"}); if (selection.is_equal_to("print")) { this.Log().Info(ChocolateyLoggers.Important, "------ BEGIN SCRIPT ------"); this.Log().Info(() => "{0}{1}{0}".format_with(Environment.NewLine, chocoPowerShellScriptContents.escape_curly_braces())); this.Log().Info(ChocolateyLoggers.Important, "------- END SCRIPT -------"); - selection = InteractivePrompt.prompt_for_confirmation(@"Do you want to run this script?", new[] { "yes", "no" }, defaultChoice: null, requireAnswer: true); + selection = InteractivePrompt.prompt_for_confirmation_short(@"Do you want to run this script?", new[] { "yes", "no" }); } + if (selection.is_equal_to("yes")) shouldRun = true; if (selection.is_equal_to("no")) { diff --git a/src/chocolatey/infrastructure/adapters/Console.cs b/src/chocolatey/infrastructure/adapters/Console.cs index 4bff68365b..ef1492b876 100644 --- a/src/chocolatey/infrastructure/adapters/Console.cs +++ b/src/chocolatey/infrastructure/adapters/Console.cs @@ -19,6 +19,11 @@ namespace chocolatey.infrastructure.adapters public sealed class Console : IConsole { + public void Write(string value) + { + System.Console.Write(value); + } + public string ReadLine() { return System.Console.ReadLine(); diff --git a/src/chocolatey/infrastructure/adapters/IConsole.cs b/src/chocolatey/infrastructure/adapters/IConsole.cs index 4649af2097..e7815048aa 100644 --- a/src/chocolatey/infrastructure/adapters/IConsole.cs +++ b/src/chocolatey/infrastructure/adapters/IConsole.cs @@ -19,33 +19,13 @@ namespace chocolatey.infrastructure.adapters // ReSharper disable InconsistentNaming + /// + /// Adapter for + /// public interface IConsole { - /// - /// Reads the next line of characters from the standard input stream. - /// - /// - /// The next line of characters from the input stream, or null if no more lines are available. - /// - /// - /// An I/O error occurred. - /// - /// - /// There is insufficient memory to allocate a buffer for the returned string. - /// - /// - /// The number of characters in the next line of characters is greater than . - /// - /// 1 + void Write(string value); string ReadLine(); - - /// - /// Gets the standard error output stream. - /// - /// - /// A that represents the standard error output stream. - /// - /// 1 TextWriter Error { get; } } diff --git a/src/chocolatey/infrastructure/commandline/InteractivePrompt.cs b/src/chocolatey/infrastructure/commandline/InteractivePrompt.cs index 4f5eed890b..a0834acf31 100644 --- a/src/chocolatey/infrastructure/commandline/InteractivePrompt.cs +++ b/src/chocolatey/infrastructure/commandline/InteractivePrompt.cs @@ -39,6 +39,48 @@ private static IConsole Console get { return _console.Value; } } + public static string prompt_for_confirmation_short(string prompt, IEnumerable choices, int repeat = 10) + { + if (repeat < 0) throw new ApplicationException("Too many bad attempts. Stopping before application crash."); + Ensure.that(() => prompt).is_not_null(); + Ensure.that(() => choices).is_not_null(); + Ensure + .that(() => choices) + .meets( + c => c.Any(), + (name, value) => { throw new ApplicationException("No choices passed in. Please ensure you pass choices."); }); + Ensure + .that(() => choices) + .meets( + c => !c.Any(String.IsNullOrWhiteSpace), + (name, value) => { throw new ApplicationException("Some choices are empty. Please ensure you provide no empty choices."); }); + Ensure + .that(() => choices) + .meets( + c => c.Select(entry => entry.FirstOrDefault()).Distinct().Count() == c.Count(), + (name, value) => { throw new ApplicationException("Multiple choices have the same first letter. Please ensure you pass choices with different first letters."); }); + + var promptWithChoices = "{0} ({1}): ".format_with(prompt, String.Join("/", choices)); + + Console.Write(promptWithChoices); + var selection = Console.ReadLine(); + + "chocolatey".Log().Info(ChocolateyLoggers.LogFileOnly, "{0}{1}".format_with(promptWithChoices, selection)); + + // check to see if value was passed + foreach (var choice in choices) + { + if (choice.is_equal_to(selection) || choice.Substring(0, 1).is_equal_to(selection)) + { + selection = choice; + return selection; + } + } + + "chocolatey".Log().Error(ChocolateyLoggers.Important, "Your choice of '{0}' is not a valid selection.".format_with(selection)); + return prompt_for_confirmation_short(prompt, choices, repeat - 1); + } + public static string prompt_for_confirmation(string prompt, IEnumerable choices, string defaultChoice, bool requireAnswer, int repeat = 10) { if (repeat < 0) throw new ApplicationException("Too many bad attempts. Stopping before application crash."); @@ -69,6 +111,7 @@ public static string prompt_for_confirmation(string prompt, IEnumerable counter++; } + Console.Write("> "); var selection = Console.ReadLine(); if (string.IsNullOrWhiteSpace(selection) && defaultChoice != null) diff --git a/src/chocolatey/infrastructure/logging/ChocolateyLoggers.cs b/src/chocolatey/infrastructure/logging/ChocolateyLoggers.cs index dbd59b4de6..6e7deffde9 100644 --- a/src/chocolatey/infrastructure/logging/ChocolateyLoggers.cs +++ b/src/chocolatey/infrastructure/logging/ChocolateyLoggers.cs @@ -20,5 +20,7 @@ public enum ChocolateyLoggers Normal, Verbose, Important, + // Used to output prompt results in log file, but not in the console + LogFileOnly, } } \ No newline at end of file diff --git a/src/chocolatey/infrastructure/logging/log4net.config.xml b/src/chocolatey/infrastructure/logging/log4net.config.xml index 223ddca318..1678a906ba 100644 --- a/src/chocolatey/infrastructure/logging/log4net.config.xml +++ b/src/chocolatey/infrastructure/logging/log4net.config.xml @@ -122,6 +122,10 @@ + + + +