using System.Text; using Nonemm.Digital; namespace Nonemm.Digital.Tests; /// The text waiting to go out, fed to the engine a character at a time. The /// pump is run at a baud rate no radio uses so the tests do not wait for RTTY. public class TypeAheadTests { private static readonly TimeSpan Patience = TimeSpan.FromSeconds(5); /// Fast enough that a message goes out in milliseconds, slow enough that a /// test can still catch the pump partway through. private const double Fast = 7500; private readonly StringBuilder went = new(); private string Sent { get { lock (went) { return went.ToString(); } } } private TypeAhead Buffer(double baud = Fast) => new( (character, _) => { lock (went) { went.Append(character); } return Task.CompletedTask; }, baud); private static async Task WaitForAsync(Func ready) { DateTime giveUp = DateTime.UtcNow + Patience; while (!ready() && DateTime.UtcNow < giveUp) { await Task.Delay(2); } } [Fact] public async Task AMessageGoesOutOneCharacterAtATime() { using TypeAhead buffer = Buffer(); buffer.Append("CQ TEST"); await WaitForAsync(() => Sent == "CQ TEST"); Assert.Equal("CQ TEST", Sent); Assert.Equal("CQ TEST", buffer.Sent); Assert.Equal("", buffer.Pending); } [Fact] public async Task WhatHasGoneOutIsNotPendingAnyMore() { using TypeAhead buffer = Buffer(baud: 40); buffer.Append("CQ TEST DE OM5M"); await WaitForAsync(() => Sent.Length >= 2); Assert.StartsWith(buffer.Sent, "CQ TEST DE OM5M", StringComparison.Ordinal); Assert.Equal("CQ TEST DE OM5M", buffer.Sent + buffer.Pending); } [Fact] public async Task TheOperatorRewritesWhatHasNotGoneOut() { using TypeAhead buffer = Buffer(baud: 60); buffer.Append("OM5X 599 001"); await WaitForAsync(() => buffer.Sent.Length >= 5); buffer.Rewrite("599 002", TypeAhead.NoCursor); await WaitForAsync(() => buffer.Pending.Length == 0); Assert.EndsWith("599 002", Sent, StringComparison.Ordinal); Assert.DoesNotContain("001", Sent, StringComparison.Ordinal); } [Fact] public async Task NothingGoesOutFromBehindTheCursor() { using TypeAhead buffer = Buffer(); buffer.Rewrite("CQ TEST", 2); await WaitForAsync(() => Sent.Length == 2); await Task.Delay(50); Assert.Equal("CQ", Sent); Assert.Equal(" TEST", buffer.Pending); } [Fact] public async Task TheRestGoesOutWhenTheCursorMovesOn() { using TypeAhead buffer = Buffer(); buffer.Rewrite("CQ TEST", 2); await WaitForAsync(() => Sent.Length == 2); buffer.Cursor = TypeAhead.NoCursor; await WaitForAsync(() => Sent == "CQ TEST"); Assert.Equal("CQ TEST", Sent); } [Fact] public async Task TextAddedBehindTheCursorStillGoesOut() { using TypeAhead buffer = Buffer(); buffer.Rewrite("CQ", 2); await WaitForAsync(() => Sent == "CQ"); buffer.Append(" TEST"); await WaitForAsync(() => Sent == "CQ TEST"); Assert.Equal("CQ TEST", Sent); } [Fact] public async Task TheBufferSaysWhenTheMessageHasGoneOut() { using TypeAhead buffer = Buffer(); int drained = 0; buffer.Drained += (_, _) => Interlocked.Increment(ref drained); buffer.Append("TU"); await WaitForAsync(() => Volatile.Read(ref drained) == 1); Assert.Equal(1, Volatile.Read(ref drained)); } [Fact] public async Task AMessageAddedWhileOneIsGoingOutFollowsIt() { using TypeAhead buffer = Buffer(baud: 400); buffer.Append("CQ "); buffer.Append("DE OM5M"); await WaitForAsync(() => buffer.Pending.Length == 0); Assert.Equal("CQ DE OM5M", Sent); } [Fact] public async Task DroppingLeavesWhatHasAlreadyGone() { using TypeAhead buffer = Buffer(baud: 60); buffer.Append("CQ TEST DE OM5M"); await WaitForAsync(() => buffer.Sent.Length >= 3); buffer.Drop(); string gone = Sent; await Task.Delay(60); Assert.Equal(gone, Sent); Assert.Equal("", buffer.Pending); Assert.Equal(gone, buffer.Sent); } [Fact] public void ClearingEmptiesBothHalves() { using TypeAhead buffer = Buffer(baud: 1); buffer.Append("CQ TEST"); buffer.Clear(); Assert.Equal("", buffer.Pending); Assert.Equal("", buffer.Sent); } [Fact] public void ACharacterTakesAsLongAsTheBaudRateSays() { using TypeAhead buffer = Buffer(baud: TypeAhead.DefaultBaud); Assert.Equal(165, buffer.CharacterTime.TotalMilliseconds, 0.5); } }