Browse Source

Double up all tests so that generic and string versions are both tested

tznind 10 months ago
parent
commit
c99487f313

+ 136 - 232
Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs

@@ -2,7 +2,7 @@
 
 namespace Terminal.Gui;
 
-  // Enum to manage the parser's state
+    // Enum to manage the parser's state
     internal enum ParserState
     {
         Normal,
@@ -10,31 +10,31 @@ namespace Terminal.Gui;
         InResponse
     }
 
-internal abstract class AnsiResponseParserBase
-{
-    protected readonly List<(string terminator, Action<string> response)> expectedResponses = new ();
-
-    // Current state of the parser
-    private ParserState _state = ParserState.Normal;
-    public ParserState State
+    internal abstract class AnsiResponseParserBase
     {
-        get => _state;
-        protected set
+        protected readonly List<(string terminator, Action<string> response)> expectedResponses = new ();
+        private ParserState _state = ParserState.Normal;
+
+        // Current state of the parser
+        public ParserState State
         {
-            StateChangedAt = DateTime.Now;
-            _state = value;
+            get => _state;
+            protected set
+            {
+                StateChangedAt = DateTime.Now;
+                _state = value;
+            }
         }
-    }
 
-    /// <summary>
-    /// When <see cref="State"/> was last changed.
-    /// </summary>
-    public DateTime StateChangedAt { get; private set; } = DateTime.Now;
+        /// <summary>
+        /// When <see cref="State"/> was last changed.
+        /// </summary>
+        public DateTime StateChangedAt { get; private set; } = DateTime.Now;
 
-    protected readonly HashSet<char> _knownTerminators = new ();
+        protected readonly HashSet<char> _knownTerminators = new ();
 
-    public AnsiResponseParserBase ()
-    {
+        public AnsiResponseParserBase ()
+        {
 
         // These all are valid terminators on ansi responses,
         // see CSI in https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s
@@ -93,248 +93,152 @@ internal abstract class AnsiResponseParserBase
         _knownTerminators.Add ('x');
         _knownTerminators.Add ('y');
         _knownTerminators.Add ('z');
-    }
-
-    // Reset the parser's state
-    protected void ResetState ()
-    {
-        State = ParserState.Normal;
-        ClearHeld ();
-    }
-
-    public abstract void ClearHeld ();
-
-    protected abstract string HeldToString ();
-
-    protected void DispatchResponse (Action<string> response)
-    {
-        response?.Invoke (HeldToString ());
-        ResetState ();
-    }
+        }
 
-    // Common response handler logic
-    protected bool ShouldReleaseHeldContent ()
-    {
-        string cur = HeldToString ();
+        protected void ResetState ()
+        {
+            State = ParserState.Normal;
+            ClearHeld ();
+        }
 
-        // Check for expected responses
-        (string terminator, Action<string> response) matchingResponse = expectedResponses.FirstOrDefault (r => cur.EndsWith (r.terminator));
+        public abstract void ClearHeld ();
+        protected abstract string HeldToString ();
+        protected abstract void AddToHeld (char c);
 
-        if (matchingResponse.response != null)
+        // Base method for processing input
+        public void ProcessInputBase (Func<int, char> getCharAtIndex, Action<char> appendOutput, int inputLength)
         {
-            DispatchResponse (matchingResponse.response);
-            expectedResponses.Remove (matchingResponse);
-            return false;
+            var index = 0; // Tracks position in the input string
+
+            while (index < inputLength)
+            {
+                var currentChar = getCharAtIndex (index);
+
+                switch (State)
+                {
+                    case ParserState.Normal:
+                        if (currentChar == '\x1B')
+                        {
+                            // Escape character detected, move to ExpectingBracket state
+                            State = ParserState.ExpectingBracket;
+                            AddToHeld (currentChar); // Hold the escape character
+                        }
+                        else
+                        {
+                            // Normal character, append to output
+                            appendOutput (currentChar);
+                        }
+                        break;
+
+                    case ParserState.ExpectingBracket:
+                        if (currentChar == '[')
+                        {
+                            // Detected '[', transition to InResponse state
+                            State = ParserState.InResponse;
+                            AddToHeld (currentChar); // Hold the '['
+                        }
+                        else
+                        {
+                            // Invalid sequence, release held characters and reset to Normal
+                            ReleaseHeld (appendOutput);
+                            appendOutput (currentChar); // Add current character
+                            ResetState ();
+                        }
+                        break;
+
+                    case ParserState.InResponse:
+                        AddToHeld (currentChar);
+
+                        // Check if the held content should be released
+                        if (ShouldReleaseHeldContent ())
+                        {
+                            ReleaseHeld (appendOutput);
+                            ResetState (); // Exit response mode and reset
+                        }
+                        break;
+                }
+
+                index++;
+            }
         }
 
-        if (_knownTerminators.Contains (cur.Last ()) && cur.StartsWith (EscSeqUtils.CSI))
+        private void ReleaseHeld (Action<char> appendOutput)
         {
-            // Detected a response that was not expected
-            return true;
+            foreach (var c in HeldToString ())
+            {
+                appendOutput (c);
+            }
         }
 
-        return false; // Continue accumulating
-    }
-
-    /// <summary>
-    ///     Registers a new expected ANSI response with a specific terminator and a callback for when the response is
-    ///     completed.
-    /// </summary>
-    public void ExpectResponse (string terminator, Action<string> response) { expectedResponses.Add ((terminator, response)); }
-}
-
-
-internal class AnsiResponseParser<T> : AnsiResponseParserBase
-{
-    private readonly List<Tuple<char,T>> held = new ();
+        // Common response handler logic
+        protected bool ShouldReleaseHeldContent ()
+        {
+            string cur = HeldToString ();
 
-    /// <summary>
-    ///     Processes input which may be a single character or multiple.
-    ///     Returns what should be passed on to any downstream input processing
-    ///     (i.e., removes expected ANSI responses from the input stream).
-    /// </summary>
-    public IEnumerable<Tuple<char,T>> ProcessInput (params Tuple<char,T>[] input)
-    {
-        var output = new List<Tuple<char, T>> (); // Holds characters that should pass through
-        var index = 0; // Tracks position in the input string
+            // Check for expected responses
+            (string terminator, Action<string> response) matchingResponse = expectedResponses.FirstOrDefault (r => cur.EndsWith (r.terminator));
 
-        while (index < input.Length)
-        {
-            var currentChar = input [index];
+            if (matchingResponse.response != null)
+            {
+                DispatchResponse (matchingResponse.response);
+                expectedResponses.Remove (matchingResponse);
+                return false;
+            }
 
-            switch (State)
+            if (_knownTerminators.Contains (cur.Last ()) && cur.StartsWith (EscSeqUtils.CSI))
             {
-                case ParserState.Normal:
-                    if (currentChar.Item1 == '\x1B')
-                    {
-                        // Escape character detected, move to ExpectingBracket state
-                        State = ParserState.ExpectingBracket;
-                        held.Add (currentChar); // Hold the escape character
-                        index++;
-                    }
-                    else
-                    {
-                        // Normal character, append to output
-                        output.Add (currentChar);
-                        index++;
-                    }
-
-                    break;
-
-                case ParserState.ExpectingBracket:
-                    if (currentChar.Item1 == '[')
-                    {
-                        // Detected '[' , transition to InResponse state
-                        State = ParserState.InResponse;
-                        held.Add (currentChar); // Hold the '['
-                        index++;
-                    }
-                    else
-                    {
-                        // Invalid sequence, release held characters and reset to Normal
-                        output.AddRange (held);
-                        output.Add (currentChar); // Add current character
-                        ResetState ();
-                        index++;
-                    }
-
-                    break;
-
-                case ParserState.InResponse:
-                    held.Add (currentChar);
-
-                    // Check if the held content should be released
-                    if (ShouldReleaseHeldContent ())
-                    {
-                        output.AddRange (held);
-                        ResetState (); // Exit response mode and reset
-                    }
-
-                    index++;
-
-                    break;
+                // Detected a response that was not expected
+                return true;
             }
+
+            return false; // Continue accumulating
         }
 
-        return output; // Return all characters that passed through
-    }
 
-    /// <summary>
-    ///     Resets the parser's state when a response is handled or finished.
-    /// </summary>
-    private void ResetState ()
-    {
-        State = ParserState.Normal;
-        held.Clear ();
-    }
+        protected void DispatchResponse (Action<string> response)
+        {
+            response?.Invoke (HeldToString ());
+            ResetState ();
+        }
 
-    /// <inheritdoc />
-    public override void ClearHeld ()
-    {
-        held.Clear ();
+        /// <summary>
+        ///     Registers a new expected ANSI response with a specific terminator and a callback for when the response is completed.
+        /// </summary>
+        public void ExpectResponse (string terminator, Action<string> response) => expectedResponses.Add ((terminator, response));
     }
 
-    protected override string HeldToString ()
+    internal class AnsiResponseParser<T> : AnsiResponseParserBase
     {
-        return new string (held.Select (h => h.Item1).ToArray ());
-    }
-}
+        private readonly List<Tuple<char, T>> held = new ();
 
+        public IEnumerable<Tuple<char, T>> ProcessInput (params Tuple<char, T> [] input)
+        {
+            var output = new List<Tuple<char, T>> ();
+            ProcessInputBase (i => input [i].Item1, c => output.Add (new Tuple<char, T> (c, input [0].Item2)), input.Length);
+            return output;
+        }
 
+        public override void ClearHeld () => held.Clear ();
 
+        protected override string HeldToString () => new string (held.Select (h => h.Item1).ToArray ());
 
-internal class AnsiResponseParser : AnsiResponseParserBase
-{
-    private readonly StringBuilder held = new ();
+        protected override void AddToHeld (char c) => held.Add (new Tuple<char, T> (c, default!));
+    }
 
-    /// <summary>
-    ///     Processes input which may be a single character or multiple.
-    ///     Returns what should be passed on to any downstream input processing
-    ///     (i.e., removes expected ANSI responses from the input stream).
-    /// </summary>
-    public string ProcessInput (string input)
+    internal class AnsiResponseParser : AnsiResponseParserBase
     {
-        var output = new StringBuilder (); // Holds characters that should pass through
-        var index = 0; // Tracks position in the input string
+        private readonly StringBuilder held = new ();
 
-        while (index < input.Length)
+        public string ProcessInput (string input)
         {
-            var currentChar = input [index];
-
-            switch (State)
-            {
-                case ParserState.Normal:
-                    if (currentChar == '\x1B')
-                    {
-                        // Escape character detected, move to ExpectingBracket state
-                        State = ParserState.ExpectingBracket;
-                        held.Append (currentChar); // Hold the escape character
-                        index++;
-                    }
-                    else
-                    {
-                        // Normal character, append to output
-                        output.Append (currentChar);
-                        index++;
-                    }
-
-                    break;
-
-                case ParserState.ExpectingBracket:
-                    if (currentChar == '[')
-                    {
-                        // Detected '[' , transition to InResponse state
-                        State = ParserState.InResponse;
-                        held.Append (currentChar); // Hold the '['
-                        index++;
-                    }
-                    else
-                    {
-                        // Invalid sequence, release held characters and reset to Normal
-                        output.Append (held);
-                        output.Append (currentChar); // Add current character
-                        ResetState ();
-                        index++;
-                    }
-
-                    break;
-
-                case ParserState.InResponse:
-                    held.Append (currentChar);
-
-                    // Check if the held content should be released
-                    if (ShouldReleaseHeldContent ())
-                    {
-                        output.Append (held);
-                        ResetState (); // Exit response mode and reset
-                    }
-
-                    index++;
-
-                    break;
-            }
+            var output = new StringBuilder ();
+            ProcessInputBase (i => input [i], c => output.Append (c), input.Length);
+            return output.ToString ();
         }
 
-        return output.ToString(); // Return all characters that passed through
-    }
-
-    /// <summary>
-    ///     Resets the parser's state when a response is handled or finished.
-    /// </summary>
-    private void ResetState ()
-    {
-        State = ParserState.Normal;
-        held.Clear ();
-    }
+        public override void ClearHeld () => held.Clear ();
 
-    /// <inheritdoc />
-    public override void ClearHeld ()
-    {
-        held.Clear ();
-    }
+        protected override string HeldToString () => held.ToString ();
 
-    protected override string HeldToString ()
-    {
-        return held.ToString ();
-    }
-}
+        protected override void AddToHeld (char c) => held.Append (c);
+    }

+ 47 - 21
UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs

@@ -1,12 +1,14 @@
 using System.Diagnostics;
 using System.Text;
 using Microsoft.VisualStudio.TestPlatform.Utilities;
+using Terminal.Gui;
 using Xunit.Abstractions;
 
 namespace UnitTests.ConsoleDrivers;
 public class AnsiResponseParserTests (ITestOutputHelper output)
 {
-    AnsiResponseParser<int> _parser = new AnsiResponseParser<int> ();
+    AnsiResponseParser<int> _parser1 = new AnsiResponseParser<int> ();
+    AnsiResponseParser _parser2 = new AnsiResponseParser ();
 
     [Fact]
     public void TestInputProcessing ()
@@ -16,12 +18,14 @@ public class AnsiResponseParserTests (ITestOutputHelper output)
                             "\x1B[0c";            // Device Attributes response (e.g., terminal identification i.e. DAR)
 
 
-        string? response = null;
+        string? response1 = null;
+        string? response2 = null;
 
         int i = 0;
 
         // Imagine that we are expecting a DAR
-        _parser.ExpectResponse ("c",(s)=> response = s);
+        _parser1.ExpectResponse ("c",(s)=> response1 = s);
+        _parser2.ExpectResponse ("c", (s) => response2 = s);
 
         // First char is Escape which we must consume incase what follows is the DAR
         AssertConsumed (ansiStream, ref i); // Esc
@@ -47,10 +51,13 @@ public class AnsiResponseParserTests (ITestOutputHelper output)
         }
 
         // Consume the terminator 'c' and expect this to call the above event
-        Assert.Null (response);
+        Assert.Null (response1);
+        Assert.Null (response1);
         AssertConsumed (ansiStream, ref i);
-        Assert.NotNull (response);
-        Assert.Equal ("\x1B[0c", response);
+        Assert.NotNull (response2);
+        Assert.Equal ("\x1B[0c", response2);
+        Assert.NotNull (response2);
+        Assert.Equal ("\x1B[0c", response2);
     }
 
     [Theory]
@@ -97,30 +104,38 @@ public class AnsiResponseParserTests (ITestOutputHelper output)
         var swGenBatches = Stopwatch.StartNew ();
         int tests = 0;
 
-        var permutations = GetBatchPermutations (ansiStream,7).ToArray ();
+        var permutations = GetBatchPermutations (ansiStream,5).ToArray ();
 
         swGenBatches.Stop ();
         var swRunTest = Stopwatch.StartNew ();
 
         foreach (var batchSet in permutations)
         {
-            string response = string.Empty;
+            string response1 = string.Empty;
+            string response2 = string.Empty;
 
             // Register the expected response with the given terminator
-            _parser.ExpectResponse (expectedTerminator, s => response = s);
+            _parser1.ExpectResponse (expectedTerminator, s => response1 = s);
+            _parser2.ExpectResponse (expectedTerminator, s => response2 = s);
 
             // Process the input
-            StringBuilder actualOutput = new StringBuilder ();
+            StringBuilder actualOutput1 = new StringBuilder ();
+            StringBuilder actualOutput2 = new StringBuilder ();
 
             foreach (var batch in batchSet)
             {
-                var output = _parser.ProcessInput (StringToBatch (batch));
-                actualOutput.Append (BatchToString (output));
+                var output1 = _parser1.ProcessInput (StringToBatch (batch));
+                actualOutput1.Append (BatchToString (output1));
+
+                var output2 = _parser2.ProcessInput (batch);
+                actualOutput2.Append (output2);
             }
 
             // Assert the final output minus the expected response
-            Assert.Equal (expectedOutput, actualOutput.ToString());
-            Assert.Equal (expectedResponse, response);
+            Assert.Equal (expectedOutput, actualOutput1.ToString());
+            Assert.Equal (expectedResponse, response1);
+            Assert.Equal (expectedOutput, actualOutput2.ToString ());
+            Assert.Equal (expectedResponse, response2);
             tests++;
         }
 
@@ -175,25 +190,36 @@ public class AnsiResponseParserTests (ITestOutputHelper output)
 
     private void AssertIgnored (string ansiStream,char expected, ref int i)
     {
-        var c = NextChar (ansiStream, ref i);
+        var c2 = ansiStream [i];
+        var c1 = NextChar (ansiStream, ref i);
 
         // Parser does not grab this key (i.e. driver can continue with regular operations)
-        Assert.Equal ( c,_parser.ProcessInput (c));
-        Assert.Equal (expected,c.Single().Item1);
+        Assert.Equal ( c1,_parser1.ProcessInput (c1));
+        Assert.Equal (expected,c1.Single().Item1);
+
+        Assert.Equal (c2, _parser2.ProcessInput (c2.ToString()).Single());
+        Assert.Equal (expected, c2 );
     }
     private void AssertConsumed (string ansiStream, ref int i)
     {
         // Parser grabs this key
-        var c = NextChar (ansiStream, ref i);
-        Assert.Empty (_parser.ProcessInput(c));
+        var c2 = ansiStream [i];
+        var c1 = NextChar (ansiStream, ref i);
+
+        Assert.Empty (_parser1.ProcessInput(c1));
+        Assert.Empty (_parser2.ProcessInput (c2.ToString()));
     }
     private void AssertReleased (string ansiStream, ref int i, string expectedRelease)
     {
-        var c = NextChar (ansiStream, ref i);
+        var c2 = ansiStream [i];
+        var c1 = NextChar (ansiStream, ref i);
 
         // Parser realizes it has grabbed content that does not belong to an outstanding request
         // Parser returns false to indicate to continue
-        Assert.Equal(expectedRelease,BatchToString(_parser.ProcessInput (c)));
+        Assert.Equal(expectedRelease,BatchToString(_parser1.ProcessInput (c1)));
+
+
+        Assert.Equal (expectedRelease, _parser2.ProcessInput (c2.ToString ()));
     }
 
     private string BatchToString (IEnumerable<Tuple<char, int>> processInput)