using System.Text; using Nonemm.Digital; namespace Nonemm.Digital.Tests; /// The text waiting to go out, fed to the engine a few characters 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; /// A character time of 100 ms, so a test can tell the characters sent ahead /// from the ones that wait for the engine. private const double Slow = 75; 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 AMessageGoesOut() { 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); } /// The engine is given the second character before it has transmitted the /// first, so it has one in hand when the first is done. An engine left with /// an empty buffer transmits idle instead, and that idle is added to how /// long the message takes. [Fact] public async Task TheEngineIsKeptOneCharacterAhead() { using TypeAhead buffer = Buffer(baud: Slow); buffer.Append("CQ TEST"); await WaitForAsync(() => Sent.Length >= 2); Assert.Equal("CQ", Sent); } /// Only the lead goes out ahead. The rest waits, which is what leaves it /// where the operator can still change it. [Fact] public async Task NoMoreThanTheLeadGoesToTheEngineAtOnce() { using TypeAhead buffer = Buffer(baud: Slow); buffer.Append("CQ TEST"); await WaitForAsync(() => Sent.Length >= 2); await Task.Delay(20); Assert.Equal(2, Sent.Length); } /// The estimate of what the engine still holds is only an estimate. An /// engine that says it has stopped transmitting has an empty buffer, and /// the next character goes to it at once rather than a character time /// later. [Fact] public async Task AnIdleEngineIsFedWithoutWaiting() { using TypeAhead buffer = Buffer(baud: Slow); buffer.Append("CQ TEST"); await WaitForAsync(() => Sent.Length >= 2); buffer.EngineIdle(); await WaitForAsync(() => Sent.Length >= 4); Assert.Equal("CQ T", Sent); } [Fact] public void ACharacterTakesAsLongAsTheBaudRateSays() { using TypeAhead buffer = Buffer(baud: TypeAhead.DefaultBaud); Assert.Equal(165, buffer.CharacterTime.TotalMilliseconds, 0.5); } }