Browse Source

Start adding tests for AnsiRequestScheduler

tznind 9 months ago
parent
commit
1d8e7563fd

+ 28 - 19
Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs

@@ -12,9 +12,16 @@ namespace Terminal.Gui;
 internal class AnsiRequestScheduler
 {
     private readonly IAnsiResponseParser _parser;
-    private readonly Func<DateTime> _now;
 
-    private readonly List<Tuple<AnsiEscapeSequenceRequest, DateTime>> _requests = new ();
+    /// <summary>
+    /// Function for returning the current time. Use in unit tests to
+    /// ensure repeatable tests.
+    /// </summary>
+    internal Func<DateTime> Now { get; set; }
+
+    private readonly List<Tuple<AnsiEscapeSequenceRequest, DateTime>> _queuedRequests = new ();
+
+    internal IReadOnlyCollection<AnsiEscapeSequenceRequest> QueuedRequests => _queuedRequests.Select (r => r.Item1).ToList ();
 
     /// <summary>
     ///     <para>
@@ -24,7 +31,7 @@ internal class AnsiRequestScheduler
     ///         regular screen drawing / mouse events etc to come in.
     ///     </para>
     ///     <para>
-    ///         When user exceeds the throttle, new requests accumulate in <see cref="_requests"/> (i.e. remain
+    ///         When user exceeds the throttle, new requests accumulate in <see cref="_queuedRequests"/> (i.e. remain
     ///         queued).
     ///     </para>
     /// </summary>
@@ -50,8 +57,8 @@ internal class AnsiRequestScheduler
     public AnsiRequestScheduler (IAnsiResponseParser parser, Func<DateTime>? now = null)
     {
         _parser = parser;
-        _now = now ?? (() => DateTime.Now);
-        _lastRun = _now ();
+        Now = now ?? (() => DateTime.Now);
+        _lastRun = Now ();
     }
 
     /// <summary>
@@ -71,18 +78,20 @@ internal class AnsiRequestScheduler
 
         if (reason == ReasonCannotSend.OutstandingRequest)
         {
-            EvictStaleRequests (request.Terminator);
-
-            // Try again after evicting
-            if (CanSend (request, out _))
+            // If we can evict an old request (no response from terminal after ages)
+            if (EvictStaleRequests (request.Terminator))
             {
-                Send (request);
+                // Try again after evicting
+                if (CanSend (request, out _))
+                {
+                    Send (request);
 
-                return true;
+                    return true;
+                }
             }
         }
 
-        _requests.Add (Tuple.Create (request, _now ()));
+        _queuedRequests.Add (Tuple.Create (request, Now ()));
 
         return false;
     }
@@ -98,7 +107,7 @@ internal class AnsiRequestScheduler
     {
         if (_lastSend.TryGetValue (withTerminator, out DateTime dt))
         {
-            if (_now () - dt > _staleTimeout)
+            if (Now () - dt > _staleTimeout)
             {
                 _parser.StopExpecting (withTerminator, false);
 
@@ -110,7 +119,7 @@ internal class AnsiRequestScheduler
     }
 
     /// <summary>
-    ///     Identifies and runs any <see cref="_requests"/> that can be sent based on the
+    ///     Identifies and runs any <see cref="_queuedRequests"/> that can be sent based on the
     ///     current outstanding requests of the parser.
     /// </summary>
     /// <param name="force">
@@ -123,16 +132,16 @@ internal class AnsiRequestScheduler
     /// </returns>
     public bool RunSchedule (bool force = false)
     {
-        if (!force && _now () - _lastRun < _runScheduleThrottle)
+        if (!force && Now () - _lastRun < _runScheduleThrottle)
         {
             return false;
         }
 
-        Tuple<AnsiEscapeSequenceRequest, DateTime>? opportunity = _requests.FirstOrDefault (r => CanSend (r.Item1, out _));
+        Tuple<AnsiEscapeSequenceRequest, DateTime>? opportunity = _queuedRequests.FirstOrDefault (r => CanSend (r.Item1, out _));
 
         if (opportunity != null)
         {
-            _requests.Remove (opportunity);
+            _queuedRequests.Remove (opportunity);
             Send (opportunity.Item1);
 
             return true;
@@ -143,7 +152,7 @@ internal class AnsiRequestScheduler
 
     private void Send (AnsiEscapeSequenceRequest r)
     {
-        _lastSend.AddOrUpdate (r.Terminator, _ => _now (), (_, _) => _now ());
+        _lastSend.AddOrUpdate (r.Terminator, _ => Now (), (_, _) => Now ());
         _parser.ExpectResponse (r.Terminator, r.ResponseReceived, false);
         r.Send ();
     }
@@ -173,7 +182,7 @@ internal class AnsiRequestScheduler
     {
         if (_lastSend.TryGetValue (r.Terminator, out DateTime value))
         {
-            return _now () - value < _throttle;
+            return Now () - value < _throttle;
         }
 
         return false;

+ 74 - 0
UnitTests/ConsoleDrivers/AnsiRequestSchedulerTests.cs

@@ -0,0 +1,74 @@
+using Moq;
+
+namespace UnitTests.ConsoleDrivers;
+
+
+public class AnsiRequestSchedulerTests
+{
+    private readonly Mock<IAnsiResponseParser> _parserMock;
+    private readonly AnsiRequestScheduler _scheduler;
+
+    private static DateTime _staticNow; // Static value to hold the current time
+
+    public AnsiRequestSchedulerTests ()
+    {
+        _parserMock = new Mock<IAnsiResponseParser> (MockBehavior.Strict);
+        _staticNow = DateTime.UtcNow; // Initialize static time
+        _scheduler = new AnsiRequestScheduler (_parserMock.Object, () => _staticNow);
+    }
+
+    [Fact]
+    public void SendOrSchedule_SendsDeviceAttributeRequest_WhenNoOutstandingRequests ()
+    {
+        // Arrange
+        var request = new AnsiEscapeSequenceRequest
+        {
+            Request = "\u001b[0c", // ESC [ c
+            Terminator = "c",
+            ResponseReceived = r => { }
+        };
+
+        // we have no outstanding for c already
+        _parserMock.Setup (p => p.IsExpecting ("c")).Returns (false).Verifiable(Times.Once);
+
+        // then we should execute our request
+        _parserMock.Setup (p => p.ExpectResponse ("c", It.IsAny<Action<string>> (), false)).Verifiable (Times.Once);
+
+        // Act
+        bool result = _scheduler.SendOrSchedule (request);
+
+
+        // Assert
+        Assert.Empty (_scheduler.QueuedRequests); // We sent it i.e. we did not queue it for later
+        Assert.True (result); // Should send immediately
+        _parserMock.Verify ();
+    }
+    [Fact]
+    public void SendOrSchedule_QueuesRequest_WhenOutstandingRequestExists ()
+    {
+        // Arrange
+        var request1 = new AnsiEscapeSequenceRequest
+        {
+            Request = "\u001b[0c", // ESC [ 0 c
+            Terminator = "c",
+            ResponseReceived = r => { }
+        };
+
+        // Parser already has an ongoing request for "c"
+        _parserMock.Setup (p => p.IsExpecting ("c")).Returns (true).Verifiable (Times.Once);
+
+        // Act
+        var result = _scheduler.SendOrSchedule (request1);
+
+        // Assert
+        Assert.Single (_scheduler.QueuedRequests); // Ensure only one request is in the queue
+        Assert.False (result); // Should be queued
+        _parserMock.Verify ();
+    }
+    private void SetTime (int milliseconds)
+    {
+        // This simulates the passing of time by setting the Now function to return a specific time.
+        var newNow = _staticNow.AddMilliseconds (milliseconds);
+        _scheduler.Now = () => newNow;
+    }
+}