The pump handed the engine one character and then waited a character time
before the next, on purpose, so it stayed behind the engine rather than ahead.
That leaves the engine with an empty buffer between every two characters of a
message, and an engine with an empty buffer transmits idle rather than waiting.
The idle is added to how long the message takes, which in a contest is time
paid for nothing.
The pump now keeps two characters in the engine: one being transmitted and one
behind it, so the engine never runs dry. The clock at the baud rate says when
the engine has room for the next one, and the engine reporting that it has
stopped transmitting sets the estimate back to an empty buffer.
The last two characters are now beyond reach rather than the last one. Everything
before them can still be rewritten, which is what the pane is for.
A message is finished when the buffer is empty and the engine is estimated to
have transmitted what it holds, so {END} and {RX} no longer run while the engine
still has the last characters of the message.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RtspmWmS7f8kUvcyaHpRWZ
240 lines
6.6 KiB
C#
240 lines
6.6 KiB
C#
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<bool> 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);
|
|
}
|
|
}
|