Support for persistent expectations of responses

This commit is contained in:
tznind
2024-10-26 10:21:22 +01:00
parent 2324217397
commit eaddbc6c1d
5 changed files with 120 additions and 39 deletions

View File

@@ -76,7 +76,7 @@ public class AnsiRequestScheduler(IAnsiResponseParser parser)
{
if (DateTime.Now - dt > _staleTimeout)
{
parser.StopExpecting (withTerminator);
parser.StopExpecting (withTerminator,false);
return true;
}
@@ -118,7 +118,7 @@ public class AnsiRequestScheduler(IAnsiResponseParser parser)
private void Send (AnsiEscapeSequenceRequest r)
{
_lastSend.AddOrUpdate (r.Terminator,(s)=>DateTime.Now,(s,v)=>DateTime.Now);
parser.ExpectResponse (r.Terminator,r.ResponseReceived);
parser.ExpectResponse (r.Terminator,r.ResponseReceived,false);
r.Send ();
}

View File

@@ -0,0 +1,10 @@
#nullable enable
namespace Terminal.Gui;
public record AnsiResponseExpectation (string Terminator, Action<string> Response)
{
public bool Matches (string cur)
{
return cur.EndsWith (Terminator);
}
}

View File

@@ -4,17 +4,24 @@ using System.Runtime.ConstrainedExecution;
namespace Terminal.Gui;
internal abstract class AnsiResponseParserBase : IAnsiResponseParser
{
/// <summary>
/// Responses we are expecting to come in.
/// </summary>
protected readonly List<(string terminator, Action<string> response)> expectedResponses = new ();
protected readonly List<AnsiResponseExpectation> expectedResponses = new ();
/// <summary>
/// Collection of responses that we <see cref="StopExpecting"/>.
/// </summary>
protected readonly List<(string terminator, Action<string> response)> lateResponses = new ();
protected readonly List<AnsiResponseExpectation> lateResponses = new ();
/// <summary>
/// Responses that you want to look out for that will come in continuously e.g. mouse events.
/// Key is the terminator.
/// </summary>
protected readonly List<AnsiResponseExpectation> persistentExpectations = new ();
private AnsiResponseParserState _state = AnsiResponseParserState.Normal;
@@ -208,13 +215,28 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
string cur = HeldToString ();
// Look for an expected response for what is accumulated so far (since Esc)
if (MatchResponse (cur, expectedResponses, true))
if (MatchResponse (cur,
expectedResponses,
invokeCallback: true,
removeExpectation:true))
{
return false;
}
// Also try looking for late requests - in which case we do not invoke but still swallow content to avoid corrupting downstream
if (MatchResponse (cur, lateResponses, false))
if (MatchResponse (cur,
lateResponses,
invokeCallback: false,
removeExpectation:true))
{
return false;
}
// Look for persistent requests
if (MatchResponse (cur,
persistentExpectations,
invokeCallback: true,
removeExpectation:false))
{
return false;
}
@@ -230,20 +252,24 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
return false; // Continue accumulating
}
private bool MatchResponse (string cur, List<(string terminator, Action<string> response)> collection, bool invokeCallback)
private bool MatchResponse (string cur, List<AnsiResponseExpectation> collection, bool invokeCallback, bool removeExpectation)
{
// Check for expected responses
var matchingResponse = collection.FirstOrDefault (r => cur.EndsWith (r.terminator));
var matchingResponse = collection.FirstOrDefault (r => r.Matches(cur));
if (matchingResponse.response != null)
if (matchingResponse?.Response != null)
{
if (invokeCallback)
{
matchingResponse.response?.Invoke (HeldToString ());
matchingResponse.Response?.Invoke (HeldToString ());
}
ResetState ();
collection.Remove (matchingResponse);
if (removeExpectation)
{
collection.Remove (matchingResponse);
}
return true;
}
@@ -252,24 +278,41 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
}
/// <inheritdoc />
public void ExpectResponse (string terminator, Action<string> response) { expectedResponses.Add ((terminator, response)); }
/// <inheritdoc />
public bool IsExpecting (string requestTerminator)
public void ExpectResponse (string terminator, Action<string> response, bool persistent)
{
// If any of the new terminator matches any existing terminators characters it's a collision so true.
return expectedResponses.Any (r => r.terminator.Intersect (requestTerminator).Any());
if (persistent)
{
persistentExpectations.Add (new (terminator, response));
}
else
{
expectedResponses.Add (new (terminator, response));
}
}
/// <inheritdoc />
public void StopExpecting (string requestTerminator)
public bool IsExpecting (string terminator)
{
var removed = expectedResponses.Where (r => r.terminator == requestTerminator).ToArray ();
// If any of the new terminator matches any existing terminators characters it's a collision so true.
return expectedResponses.Any (r => r.Terminator.Intersect (terminator).Any());
}
foreach (var r in removed)
/// <inheritdoc />
public void StopExpecting (string terminator, bool persistent)
{
if (persistent)
{
expectedResponses.Remove (r);
lateResponses.Add (r);
persistentExpectations.RemoveAll (r=>r.Matches (terminator));
}
else
{
var removed = expectedResponses.Where (r => r.Terminator == terminator).ToArray ();
foreach (var r in removed)
{
expectedResponses.Remove (r);
lateResponses.Add (r);
}
}
}
}

View File

@@ -19,16 +19,21 @@ public interface IAnsiResponseParser
/// sent an ANSI request out).
/// </summary>
/// <param name="terminator">The terminator you expect to see on response.</param>
/// <param name="persistent"><see langword="true"/> if you want this to persist permanently
/// and be raised for every event matching the <paramref name="terminator"/>.</param>
/// <param name="response">Callback to invoke when the response is seen in console input.</param>
void ExpectResponse (string terminator, Action<string> response);
/// <exception cref="ArgumentException">If trying to register a persistent request for a terminator
/// that already has one.
/// exists.</exception>
void ExpectResponse (string terminator, Action<string> response, bool persistent);
/// <summary>
/// Returns true if there is an existing expectation (i.e. we are waiting a response
/// from console) for the given <paramref name="requestTerminator"/>.
/// from console) for the given <paramref name="terminator"/>.
/// </summary>
/// <param name="requestTerminator"></param>
/// <param name="terminator"></param>
/// <returns></returns>
bool IsExpecting (string requestTerminator);
bool IsExpecting (string terminator);
/// <summary>
/// Removes callback and expectation that we will get a response for the
@@ -36,5 +41,7 @@ public interface IAnsiResponseParser
/// requests e.g. if you want to send a different one with the same terminator.
/// </summary>
/// <param name="requestTerminator"></param>
void StopExpecting (string requestTerminator);
}
/// <param name="persistent"><see langword="true"/> if you want to remove a persistent
/// request listener.</param>
void StopExpecting (string requestTerminator, bool persistent);
}

View File

@@ -27,8 +27,8 @@ public class AnsiResponseParserTests (ITestOutputHelper output)
int i = 0;
// Imagine that we are expecting a DAR
_parser1.ExpectResponse ("c",(s)=> response1 = s);
_parser2.ExpectResponse ("c", (s) => response2 = s);
_parser1.ExpectResponse ("c",(s)=> response1 = s, false);
_parser2.ExpectResponse ("c", (s) => response2 = s , false);
// First char is Escape which we must consume incase what follows is the DAR
AssertConsumed (ansiStream, ref i); // Esc
@@ -118,8 +118,8 @@ public class AnsiResponseParserTests (ITestOutputHelper output)
string response2 = string.Empty;
// Register the expected response with the given terminator
_parser1.ExpectResponse (expectedTerminator, s => response1 = s);
_parser2.ExpectResponse (expectedTerminator, s => response2 = s);
_parser1.ExpectResponse (expectedTerminator, s => response1 = s, false);
_parser2.ExpectResponse (expectedTerminator, s => response2 = s, false);
// Process the input
StringBuilder actualOutput1 = new StringBuilder ();
@@ -225,7 +225,7 @@ public class AnsiResponseParserTests (ITestOutputHelper output)
if (terminator.HasValue)
{
parser.ExpectResponse (terminator.Value.ToString (),(s)=> response = s);
parser.ExpectResponse (terminator.Value.ToString (),(s)=> response = s, false);
}
foreach (var state in expectedStates)
{
@@ -326,13 +326,13 @@ public class AnsiResponseParserTests (ITestOutputHelper output)
string? responseA = null;
string? responseB = null;
p.ExpectResponse ("z",(r)=>responseA=r);
p.ExpectResponse ("z",(r)=>responseA=r, false);
// Some time goes by without us seeing a response
p.StopExpecting ("z");
p.StopExpecting ("z", false);
// Send our new request
p.ExpectResponse ("z", (r) => responseB = r);
p.ExpectResponse ("z", (r) => responseB = r, false);
// Because we gave up on getting A, we should expect the response to be to our new request
Assert.Empty(p.ProcessInput ("\u001b[<1;2z"));
@@ -351,6 +351,29 @@ public class AnsiResponseParserTests (ITestOutputHelper output)
}
[Fact]
public void TestPersistentResponses ()
{
var p = new AnsiResponseParser ();
int m = 0;
int M = 1;
p.ExpectResponse ("m", _ => m++, true);
p.ExpectResponse ("M", _ => M++, true);
// Act - Feed input strings containing ANSI sequences
p.ProcessInput ("\u001b[<0;10;10m"); // Should match and increment `m`
p.ProcessInput ("\u001b[<0;20;20m"); // Should match and increment `m`
p.ProcessInput ("\u001b[<0;30;30M"); // Should match and increment `M`
p.ProcessInput ("\u001b[<0;40;40M"); // Should match and increment `M`
p.ProcessInput ("\u001b[<0;50;50M"); // Should match and increment `M`
// Assert - Verify that counters reflect the expected counts of each terminator
Assert.Equal (2, m); // Expected two `m` responses
Assert.Equal (4, M); // Expected three `M` responses plus the initial value of 1
}
private Tuple<char, int> [] StringToBatch (string batch)
{
return batch.Select ((k) => Tuple.Create (k, tIndex++)).ToArray ();
@@ -395,8 +418,6 @@ public class AnsiResponseParserTests (ITestOutputHelper output)
}
}
private void AssertIgnored (string ansiStream,char expected, ref int i)
{
var c2 = ansiStream [i];