#region Copyright 2009-2014 by Roger Knapp, Licensed under the Apache License, Version 2.0 /* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #endregion using System; using System.Collections.Generic; using NUnit.Framework; using CSharpTest.Net.Commands; using System.IO; using CSharpTest.Net.Utils; using System.ComponentModel; using System.Runtime.Serialization.Formatters.Binary; using System.Threading; using System.Diagnostics; #pragma warning disable 1591 namespace CSharpTest.Net.Library.Test { [TestFixture] public partial class TestCmdInterpreter { delegate void Action(); #region TestFixture SetUp/TearDown [TestFixtureSetUp] public virtual void Setup() { } [TestFixtureTearDown] public virtual void Teardown() { Environment.ExitCode = 0; } #endregion [DebuggerNonUserCode] private static int WindowHeight { get { int windowHeight; try { windowHeight = Console.WindowHeight; } catch (System.IO.IOException) { windowHeight = 25; } return windowHeight; } } /// Used to provide a set of test commands class TestCommands { string _data; [Option("SomeData", "SD", DefaultValue = "", Description = "Stores some data.")] public string SomeData { get { return _data; } set { _data = value; } } [Option("Other"), IgnoreMember] public string ThisIsIgnored { get { throw new NotImplementedException(); } set { } } [Command("Hidden"), IgnoreMember] public void ThisIsAlsoIgnored() { } int _otherdata = 0; [Option("Other")] [AliasName("alias")] //OptionAttribute takes precedence, but the following also works. [System.ComponentModel.DisplayName("ingored-due-to-OptionAttribute")] [System.ComponentModel.Description("description")] [System.ComponentModel.Category("category")] [System.ComponentModel.Browsable(false)] [System.ComponentModel.DefaultValue(-1)] public int OtherData { get { return _otherdata; } set { _otherdata = value; } } public string ReadOnlyDoesntAppear { get { return _data; } } public string WriteOnlyDoesntAppear { set { _data = value; } } [Command("Hidden", Visible = false)] [AliasName("myhiddencommand")] [AliasName("")] // <= ignored if null or empty public void Hidden(ICommandInterpreter ci, [AllArguments] string[] args) { Console.WriteLine("Hidden Runs: {0}", String.Join(" ", args)); ci.Run(args); } [Command( DisplayName="Count", AliasNames = new string[0], Category="", Description = "Count Description.", Visible = true )] public void Count( [Argument("number", "n", Description = "The number to count to or from.")] int number, [Argument("backwards", DefaultValue=false, Description="Count backwards")] bool backwards, // Arguments that are of type string[] can be specified more than once on a command-line // for example /t:x /t:y /t:z will result in the array string[3] { "x", "y", "z" } [Argument("text", "t", DefaultValue = new string[0], Description = "A piece of text to append")] string[] text, // any method can recieve an ICommandInterpreter, once encountered in args all remaining // must be qualified with /name= format. ICommandInterpreter ci, // any method can also take the complete argument list, however, it should always appear // after all other arguments since. It must always be decorated with [AllArugments]. Any // command with this parameter will not complain about unknown arguments. [AllArguments] string[] allargs ) { int st, end, offset; if (!backwards) { st = 1; end = number; offset = 1; } else { st = number; end = 1; offset = -1; } Random r = new Random(); for (int i = st; true; i += offset) { if( text.Length == 0 ) Console.WriteLine("{0}", i); else Console.WriteLine("{0} {1}", i, text[(i-1) % text.Length]); if (i == end) break; } } public void BlowUp([DefaultValue(false)] bool apperror) { if( apperror ) throw new ApplicationException("BlowUp"); throw new Exception("BlowUp"); } //undecorated should work just fine public void ForXtoYbyZ(int start, int end, int increment) { for (int i = start; true; i += increment) { Console.WriteLine("{0}", i); if (i == end) break; } } } class StaticTestFilter { static int lineNo = 0; // implements a filter than runs for all commands... [CommandFilter] // <= implied by method signature, exact signature required for all filters public static void AddLineNumbers(ICommandInterpreter ci, ICommandChain chain, string[] args) { string line; if (chain == null) { // not possible UNLESS you add this filter to the list of commands which is not recommended // since it would generally be easier to just add another method to handle this if/else branch // for you. However, since it is technically possible to do so this will be tested. Console.WriteLine("{0}", lineNo); } else { bool addLineNumbers = ArgumentList.Remove(ref args, "linenumbers", out line); TextWriter stdout = Console.Out; StringWriter swout = new StringWriter(); if( addLineNumbers ) Console.SetOut(swout); chain.Next(args); // <= Unless we want to prevent this command from executing, we must call next() if(addLineNumbers) { StringReader r = new StringReader(swout.ToString()); while (null != (line = r.ReadLine())) stdout.WriteLine("{0}: {1}", ++lineNo, line); } } } } private static string Capture(CommandInterpreter ci, string input) { TextWriter stdout = Console.Out, stderr = Console.Error; TextReader stdin = Console.In; try { StringWriter sw = new StringWriter(); Console.SetOut(sw); StringWriter swe = new StringWriter(); Console.SetError(swe); Console.SetIn(new StringReader(input)); ci.Prompt = String.Empty; ci.Run(Console.In); sw.WriteLine(swe.ToString()); return sw.ToString().Trim(); } finally { Console.SetOut(stdout); Console.SetError(stderr); Console.SetIn(stdin); } } [Test] public void TestAddCommands() { CommandInterpreter ci = new CommandInterpreter(DefaultCommands.None, new TestCommands()); Assert.AreEqual(2, ci.Options.Length); Assert.AreEqual("Other", ci.Options[0].DisplayName); Assert.AreEqual("SomeData", ci.Options[1].DisplayName); Assert.AreEqual(4, ci.Commands.Length); Assert.AreEqual("BlowUp", ci.Commands[0].DisplayName); Assert.AreEqual("Count", ci.Commands[1].DisplayName); // <= alpha-sorted Assert.AreEqual("ForXtoYbyZ", ci.Commands[2].DisplayName); Assert.AreEqual("Hidden", ci.Commands[3].DisplayName); foreach (ICommand c in ci.Commands) ci.RemoveCommand(c); Assert.AreEqual(0, ci.Commands.Length); ci = new CommandInterpreter(DefaultCommands.None); Assert.AreEqual(0, ci.Options.Length); Assert.AreEqual(0, ci.Commands.Length); ci.AddHandler(typeof(StaticTestFilter)); Assert.AreEqual(0, ci.Options.Length); // the type StaticTestFilter contains filters and no commands/options Assert.AreEqual(0, ci.Commands.Length); ci.AddHandler(new TestCommands()); Assert.AreEqual(2, ci.Options.Length); Assert.AreEqual(4, ci.Commands.Length); } [Test] public void TestHtmlHelp() { CommandInterpreter ci = new CommandInterpreter(DefaultCommands.Help, new TestCommands()); string helptext = ci.GetHtmlHelp(null); Assert.IsTrue(0 == helptext.IndexOf("")); Assert.IsTrue(helptext.Contains("COMMAND")); Assert.IsTrue(helptext.IndexOf("SOMEDATA",StringComparison.OrdinalIgnoreCase) >= 0); } [Test] public void TestHelpText() { CommandInterpreter ci = new CommandInterpreter(DefaultCommands.None, new TestCommands()); string helptext = Capture(ci, "Help"); Assert.AreNotEqual(0, Environment.ExitCode); Assert.IsTrue(helptext.Contains("Invalid")); Environment.ExitCode = 0; ci = new CommandInterpreter(DefaultCommands.Help, new TestCommands()); helptext = Capture(ci, "Help"); Assert.AreEqual(0, Environment.ExitCode); Assert.IsFalse(helptext.Contains("Invalid")); Assert.IsFalse(helptext.Contains("HIDDEN")); // <= not listed Assert.IsTrue(helptext.Contains("COUNT")); Assert.IsTrue(helptext.Contains("SOMEDATA")); Assert.IsTrue(helptext.Contains("FORXTOYBYZ")); //empty command-string displays help, the EXIT/QUIT are always available when running //interactive mode via .Run(TextReader), which is what Capture(...) does. Assert.AreEqual(helptext, Capture(ci, Environment.NewLine + "EXIT")); helptext = Capture(ci, "Help hidden"); // <= still has detailed help Assert.AreEqual(0, Environment.ExitCode); Assert.IsTrue(helptext.Contains("HIDDEN")); Assert.IsTrue(helptext.Contains("MYHIDDENCOMMAND")); // <= alias names display for details Assert.IsFalse(helptext.Contains("COUNT")); Assert.IsFalse(helptext.Contains("SOMEDATA")); Assert.IsFalse(helptext.Contains("FORXTOYBYZ")); helptext = Capture(ci, "Help Help"); Assert.AreEqual(0, Environment.ExitCode); Assert.IsTrue(helptext.Contains("HELP")); Assert.IsTrue(helptext.Contains("[/name=]String")); helptext = Capture(ci, "Help SOMEDATA"); Assert.AreEqual(0, Environment.ExitCode); Assert.IsTrue(helptext.Contains("SOMEDATA")); Assert.IsTrue(helptext.Contains("SD")); } [Test] public void TestSetPersistOption() { Assert.AreEqual(DefaultCommands.Get | DefaultCommands.Set | DefaultCommands.Help, DefaultCommands.Default); TestCommands cmds = new TestCommands(); CommandInterpreter ci = new CommandInterpreter(cmds); cmds.OtherData = 42; Assert.AreEqual("42", Capture(ci, "GET Other")); cmds.SomeData = "one-two-three"; Assert.AreEqual("one-two-three", Capture(ci, "GET SomeData")); string options = Capture(ci, "SET"); cmds.OtherData = 0; cmds.SomeData = String.Empty; Assert.AreEqual("0", Capture(ci, "GET Other")); Assert.AreEqual(String.Empty, Capture(ci, "GET SomeData")); TextReader input = Console.In; try { Console.SetIn(new StringReader(options));//feed the output of SET back to SET ci.Run("SET", "/readInput"); } finally { Console.SetIn(input); } //should now be restored Assert.AreEqual("42", Capture(ci, "GET Other")); Assert.AreEqual("one-two-three", Capture(ci, "GET SomeData")); } [Test] public void TestGetSetOption() { Assert.AreEqual(DefaultCommands.Get | DefaultCommands.Set | DefaultCommands.Help, DefaultCommands.Default); //defaults the DefaultCommands to Get/Set/Help via the enum value of DefaultCommands.Default CommandInterpreter ci = new CommandInterpreter(new TestCommands()); Assert.AreEqual(2, ci.Options.Length); Assert.AreEqual("Other", ci.Options[0].DisplayName); Assert.AreEqual(typeof(int), ci.Options[0].Type); Assert.AreEqual("SomeData", ci.Options[1].DisplayName); Assert.AreEqual(typeof(String), ci.Options[1].Type); TextWriter stdout = Console.Out; try { Console.SetOut(new StringWriter());// <= Get will also write to console Assert.AreEqual(-1, ci.Get("other"));//default was applied ci.Set("other", 1); Assert.AreEqual(1, ci.Get("other")); Assert.AreNotEqual("abc", ci.Get("somedata")); ci.Set("somedata", "abc"); Assert.AreEqual("abc", ci.Get("somedata")); } finally { Console.SetOut(stdout); } string result; result = Capture(ci, "GET Somedata"); Assert.AreEqual("abc", result); //Set without args lists options result = Capture(ci, "SET"); Assert.IsTrue(result.ToUpper().Contains("somedata".ToUpper())); //Set without value returns the current value result = Capture(ci, "SET somedata"); Assert.AreEqual("abc", result); result = Capture(ci, "SET somedata 123"); Assert.AreEqual("", result); result = Capture(ci, "GET somedata"); Assert.AreEqual("123", result); } [Test] public void TestCommandRun() { string result; CommandInterpreter ci = new CommandInterpreter(DefaultCommands.Get | DefaultCommands.Set, new TestCommands()); result = Capture(ci, "Count 2"); Assert.AreEqual("1\r\n2", result); result = Capture(ci, "Count /backwards 2"); Assert.AreEqual("2\r\n1", result); result = Capture(ci, "Count 2 /backwards"); Assert.AreEqual("2\r\n1", result); result = Capture(ci, "Count -n:2 /backwards:true"); Assert.AreEqual("2\r\n1", result); result = Capture(ci, "Count 2 /t:a /t:b"); Assert.AreEqual("1 a\r\n2 b", result); //Argument not found: result = Capture(ci, "Count"); Assert.AreEqual("The value for number is required.", result); //Non-ApplicationExcpetion dumps stack: ci.ErrorLevel = 0; result = Capture(ci, "BlowUp false"); Assert.AreNotEqual(0, ci.ErrorLevel); Assert.IsTrue(result.Contains("System.Exception: BlowUp"), "Expected \"System.Exception: BlowUp\" in {0}", result); //ApplicationExcpetion dumps message only: ci.ErrorLevel = 0; result = Capture(ci, "BlowUp true"); Assert.AreNotEqual(0, ci.ErrorLevel); Assert.AreEqual("BlowUp", result); } [Test] public void TestMacroExpand() { string result; CommandInterpreter ci = new CommandInterpreter(DefaultCommands.Echo | DefaultCommands.Prompt, new TestCommands()); ci.Set("SomeData", "TEST_Data"); result = Capture(ci, "ECHO $(SOMEDATA)"); Assert.AreEqual("TEST_Data", result); ci.Set("SomeData", "TEST Data"); result = Capture(ci, "ECHO $(SOMEDATA)"); Assert.AreEqual("\"TEST Data\"", result); // <= Echo will quote & escape while-space and quotes " result = Capture(ci, "ECHO $(MissingProperty)"); Assert.AreEqual("Unknown option specified: MissingProperty", result); result = Capture(ci, "ECHO $$(MissingProperty) $$(xx x$$y $$ abc"); // <= escape '$' with '$$' Assert.AreEqual("$(MissingProperty) $(xx x$y $ abc", result); // <= extra '$' was removed. } class ErrorReader : TextReader { public override string ReadLine() { throw new NotImplementedException(); } } [Test] public void TestLoopErrors() { TextWriter stdout = Console.Out, stderr = Console.Error; try { StringWriter sw = new StringWriter(); Console.SetError(sw); Console.SetOut(sw); string result; CommandInterpreter ci = new CommandInterpreter(new TestCommands()); ci.Prompt = "$(MissingProperty)"; ci.Run(new StringReader("EXIT")); result = sw.ToString(); Assert.IsTrue(result.StartsWith("Unknown option specified: MissingProperty")); ci.Prompt = String.Empty; sw.GetStringBuilder().Length = 0;//clear ci.Run(new ErrorReader()); result = sw.ToString(); Assert.IsTrue(result.StartsWith(typeof(NotImplementedException).FullName)); } finally { Console.SetOut(stdout); Console.SetError(stderr); } } [Test] public void TestCommandFilters() { string result; CommandInterpreter ci = new CommandInterpreter(DefaultCommands.None, new TestCommands(), typeof(StaticTestFilter)); Assert.AreEqual(1, ci.Filters.Length); Assert.AreEqual("AddLineNumbers", ci.Filters[0].DisplayName); result = Capture(ci, "Count 2 /linenumbers"); Assert.AreEqual("1: 1\r\n2: 2", result); int cmds = ci.Commands.Length; ci.AddCommand(ci.Filters[0]); Assert.AreEqual(cmds + 1, ci.Commands.Length); result = Capture(ci, "AddLineNumbers"); Assert.AreEqual("2", result); } private Char GetSpace() { return ' '; } [Test] public void TestBuiltInMore() { string result; CommandInterpreter ci = new CommandInterpreter( DefaultCommands.More | DefaultCommands.PipeCommands, new TestCommands()); //replace the keystroke wait ci.ReadNextCharacter = GetSpace; string input = String.Format("Count {0} | MORE", (int)(WindowHeight * 1.5)); result = Capture(ci, input); StringReader sr = new StringReader(result); int moreFound = 0; int index = 0; string line; while( null != (line = sr.ReadLine())) { if( line == "-- More --" ) moreFound++; else Assert.AreEqual((++index).ToString(), line); } Assert.AreEqual(1, moreFound); } [Test] public void TestBuiltInFind() { string result; CommandInterpreter ci = new CommandInterpreter( DefaultCommands.Echo | DefaultCommands.Find | DefaultCommands.PipeCommands, new TestCommands()); string input = String.Format("Count {0} | MORE", (int)(WindowHeight * 1.5)); result = Capture(ci, "Count 220 |FIND \"1\" |FIND \"0\" | FIND /V \"3\" | FIND /V \"4\" | FIND /V \"5\" | FIND /V \"6\" | FIND /V \"7\" | FIND /V \"8\" | FIND /V \"9\""); Assert.AreEqual("10\r\n100\r\n101\r\n102\r\n110\r\n120\r\n201\r\n210", result); result = Capture(ci, "ECHO ABC | FIND \"abc\" |"); Assert.AreEqual(String.Empty, result); result = Capture(ci, "ECHO ABC | FIND /I \"abc\" |"); Assert.AreEqual("ABC", result); } [Test] public void TestBuiltInRedirect() { string tempPath = Path.GetTempFileName(); string tempPath2 = Path.GetTempFileName(); try { string result; CommandInterpreter ci = new CommandInterpreter( DefaultCommands.Find | DefaultCommands.PipeCommands | DefaultCommands.IORedirect, new TestCommands()); //Redirect output: result = Capture(ci, "Count 100 > " + tempPath); Assert.AreEqual(String.Empty, result); Assert.AreEqual(100, File.ReadAllLines(tempPath).Length); result = Capture(ci, "Find \"1\" -f:" + tempPath + " |Find \"0\" > " + tempPath2); Assert.AreEqual(String.Empty, result); Assert.AreEqual("10\r\n100", File.ReadAllText(tempPath2).Trim()); //Redirect input: result = Capture(ci, "Find \"1\" |Find \"0\" <" + tempPath + " >" + tempPath2); Assert.AreEqual(String.Empty, result); Assert.AreEqual("10\r\n100", File.ReadAllText(tempPath2).Trim()); //Change precedence and watch it fail: Assert.IsTrue(ci.FilterPrecedence.StartsWith("<") || ci.FilterPrecedence.StartsWith(">")); ci.FilterPrecedence = ci.FilterPrecedence.TrimStart('<', '>'); result = Capture(ci, "Find \"1\" |Find \"0\" <" + tempPath + " >" + tempPath2); Assert.AreEqual(String.Empty, result); result = File.ReadAllText(tempPath2).Trim(); Assert.AreEqual("10\r\n20\r\n30\r\n40\r\n50\r\n60\r\n70\r\n80\r\n90\r\n100", result); } finally { File.Delete(tempPath); File.Delete(tempPath2); } } [Test] public void TestAttributes() { CommandInterpreter ci = new CommandInterpreter(new TestCommands()); IOption option = ci.Options[0]; //[Option("Other")] Assert.AreEqual("Other", option.DisplayName); Assert.AreEqual(typeof(int), option.Type); //[AliasName("alias")] //[System.ComponentModel.DisplayName("ingored-due-to-OptionAttribute")] Assert.AreEqual(2, option.AllNames.Length); Assert.IsTrue(new List(option.AllNames).Contains("Other")); Assert.IsTrue(new List(option.AllNames).Contains("alias")); //[System.ComponentModel.Description("description")] Assert.AreEqual("description", option.Description); //[System.ComponentModel.Category("category")] Assert.AreEqual("category", option.Category); //[System.ComponentModel.Browsable(false)] Assert.AreEqual(false, option.Visible); //[System.ComponentModel.DefaultValue(-1)] Assert.AreEqual(-1, option.Value); { CommandFilterAttribute a = new CommandFilterAttribute(); Assert.IsFalse(a.Visible); a.Visible = true; Assert.IsFalse(a.Visible); } { CommandAttribute a = new CommandAttribute(); a.DisplayName = "test"; a.AliasNames = new string[] { "alias" }; Assert.AreEqual("test,alias", String.Join(",", a.AllNames)); IDisplayInfo di = a; di.Help();//no-op } { AllArgumentsAttribute a = new AllArgumentsAttribute(); Assert.AreEqual(typeof(AllArgumentsAttribute), a.GetType()); } } [Test] public void EnsureSerializationOfException() { InterpreterException ex = null; try { throw new InterpreterException("TEST"); } catch (InterpreterException e) { ex = e; } Assert.IsNotNull(ex); BinaryFormatter bf = new BinaryFormatter(); using( MemoryStream ms = new MemoryStream() ) { bf.Serialize(ms, ex); ms.Position = 0; ex = (InterpreterException)bf.Deserialize(ms); Assert.AreEqual("TEST", ex.Message); } } [Test][ExpectedException(typeof(InvalidOperationException))] public void FailConsoleIO() { CommandInterpreter ci = new CommandInterpreter(); ci.ReadNextCharacter(); } } }