Start the transmit pane again on every new message
The macro buttons went wrong at the end of a transmission, both faults in
DigitalEngineSender:
- The engine reports the transmitter drop on its own thread.
WhenTransmitChanged took the state lock, decided the message had ended,
released the lock, and only then called Buffer.Ended(), which clears
what has gone to the engine. A macro pressed on the last character got
through StartAsync in that gap and had already flushed its own text into
Sent, so Ended() wiped the new text off the pane while the engine
transmitted it. Ended() is now called inside the same lock.
- The pane only started again when the engine reported a drop. Two macros
in a row keep the transmitter up, so that report never came and Sent
grew with every press. Everything in Sent is locked, because it is in
the engine and cannot be taken back, so the whole pane became
read-only. TypeAhead.Started() drops the last message's sent text and
keeps what was typed ahead, and StartAsync calls it whenever it keys a
new transmission.
The rest of this commit is the digital transmit work these fixes sit on:
the pane as one coloured box, the sender's three keying states, the
type-ahead feeder paced by the clock with the engine's count as a brake,
{RX} flushing what is left in one piece, and the entry window's function
keys reading the digital macros on a digital mode.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdAYHcdRktqKry7nk414TU
This commit is contained in:
404
tests/Nonemm.Digital.Tests/DigitalEngineSenderTests.cs
Normal file
404
tests/Nonemm.Digital.Tests/DigitalEngineSenderTests.cs
Normal file
@@ -0,0 +1,404 @@
|
||||
using System.Text;
|
||||
using Nonemm.Digital;
|
||||
|
||||
namespace Nonemm.Digital.Tests;
|
||||
|
||||
/// The message sender over a digital engine. The engine here reports what it
|
||||
/// was given and when it was told to stop; the pace is a baud rate no radio
|
||||
/// uses so the tests do not wait for RTTY.
|
||||
public class DigitalEngineSenderTests
|
||||
{
|
||||
/// Long enough for the stop, which waits for the engine to empty and then
|
||||
/// one character time on top of it.
|
||||
private static readonly TimeSpan Patience = TimeSpan.FromSeconds(15);
|
||||
|
||||
private const double Fast = 1500;
|
||||
|
||||
/// A quarter of a second per character, so a test can catch the engine
|
||||
/// still holding what it was handed.
|
||||
private const double Slow = 30;
|
||||
|
||||
[Fact]
|
||||
public async Task TheEngineDroppingTheTransmitterOnItsOwnDoesNotEndTheMessage()
|
||||
{
|
||||
FakeEngine engine = new();
|
||||
using DigitalEngineSender sender = new(engine, baud: Fast);
|
||||
|
||||
await sender.SendAsync("CQ TEST DE OM5M");
|
||||
await WaitForAsync(() => engine.Sent.Length >= 2);
|
||||
engine.Drop();
|
||||
|
||||
await WaitForAsync(() => engine.Sent == "CQ TEST DE OM5M");
|
||||
Assert.Equal("CQ TEST DE OM5M", engine.Sent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TheDropAfterReturnToReceiveEndsTheMessage()
|
||||
{
|
||||
FakeEngine engine = new();
|
||||
using DigitalEngineSender sender = new(engine, baud: Fast);
|
||||
bool finished = false;
|
||||
sender.Finished += (_, _) => finished = true;
|
||||
|
||||
await sender.SendAsync("CQ TEST");
|
||||
sender.ReturnToReceiveWhenSent();
|
||||
await WaitForAsync(() => engine.Stopped);
|
||||
engine.Drop();
|
||||
|
||||
await WaitForAsync(() => finished && sender.Buffer.Sent.Length == 0);
|
||||
Assert.True(finished);
|
||||
Assert.Equal("", sender.Buffer.Sent);
|
||||
Assert.Equal("CQ TEST", engine.Sent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TheEngineDroppingInTheMiddleOfAMessagePutsTheTransmitterBackUp()
|
||||
{
|
||||
FakeEngine engine = new();
|
||||
using DigitalEngineSender sender = new(engine, baud: Fast);
|
||||
|
||||
sender.Transmit();
|
||||
await sender.SendAsync("CQ TEST DE OM5M");
|
||||
await WaitForAsync(() => engine.Sent.Length >= 2);
|
||||
int keyed = engine.Keyed;
|
||||
engine.Drop();
|
||||
|
||||
await WaitForAsync(() => engine.Keyed > keyed);
|
||||
Assert.Equal(keyed + 1, engine.Keyed);
|
||||
}
|
||||
|
||||
/// MMTTY's stop does nothing when its buffer is empty already, so the
|
||||
/// stop has to reach the engine while it still holds the last characters.
|
||||
[Fact]
|
||||
public async Task ReturnToReceiveReachesTheEngineWhileItStillHoldsTheLastCharacters()
|
||||
{
|
||||
FakeEngine engine = new();
|
||||
using DigitalEngineSender sender = new(engine, baud: Slow);
|
||||
sender.Transmit();
|
||||
|
||||
await sender.SendAsync("AB");
|
||||
sender.ReturnToReceiveWhenSent();
|
||||
|
||||
await WaitForAsync(() => engine.Stopped);
|
||||
Assert.True(engine.Stopped);
|
||||
Assert.True(sender.Buffer.OnAir < 2, "the engine had already transmitted everything");
|
||||
}
|
||||
|
||||
/// MMTTY's stop leaves the transmitter up, so `{RX}` puts the key down
|
||||
/// itself once the engine has transmitted what it holds.
|
||||
[Fact]
|
||||
public async Task ReturnToReceivePutsTheKeyDownAfterTheEngineHasEmptied()
|
||||
{
|
||||
FakeEngine engine = new();
|
||||
using DigitalEngineSender sender = new(engine, baud: Slow);
|
||||
bool finished = false;
|
||||
sender.Finished += (_, _) => finished = true;
|
||||
sender.Transmit();
|
||||
|
||||
await sender.SendAsync("AB");
|
||||
sender.ReturnToReceiveWhenSent();
|
||||
|
||||
await WaitForAsync(() => engine.Released);
|
||||
Assert.True(engine.Released);
|
||||
Assert.Equal("AB", engine.Sent);
|
||||
Assert.False(engine.Aborted, "the message was cut off instead of being let finish");
|
||||
await WaitForAsync(() => finished);
|
||||
Assert.True(finished);
|
||||
}
|
||||
|
||||
/// N1MM's ending: what is left of the message goes to the engine in one
|
||||
/// piece and the engine is asked to stop with its buffer full, because
|
||||
/// `SetMmttyPTT(1)` does nothing at an engine that has been fed one
|
||||
/// character at a time and is therefore nearly empty.
|
||||
[Fact]
|
||||
public async Task ReturnToReceiveHandsTheRestOfTheMessageOverInOnePiece()
|
||||
{
|
||||
FakeEngine engine = new();
|
||||
using DigitalEngineSender sender = new(engine, baud: Slow);
|
||||
sender.Transmit();
|
||||
|
||||
await sender.SendAsync("CQ TEST DE OM5M");
|
||||
sender.ReturnToReceiveWhenSent();
|
||||
|
||||
await WaitForAsync(() => engine.Stopped);
|
||||
Assert.Equal("CQ TEST DE OM5M", engine.Sent);
|
||||
Assert.Equal("", sender.Buffer.Pending);
|
||||
Assert.Contains(engine.Pushes, push => push.Length > 1);
|
||||
}
|
||||
|
||||
/// The stop reaches the engine while the message is still in it, which at
|
||||
/// 30 baud is most of the four seconds the message takes.
|
||||
[Fact]
|
||||
public async Task TheStopReachesTheEngineWhileItStillHoldsTheMessage()
|
||||
{
|
||||
FakeEngine engine = new();
|
||||
using DigitalEngineSender sender = new(engine, baud: Slow);
|
||||
sender.Transmit();
|
||||
|
||||
await sender.SendAsync("CQ TEST DE OM5M");
|
||||
sender.ReturnToReceiveWhenSent();
|
||||
|
||||
await WaitForAsync(() => engine.Stopped);
|
||||
Assert.True(sender.Buffer.OnAir < "CQ TEST DE OM5M".Length,
|
||||
"the engine had transmitted the whole message before it was told to stop");
|
||||
}
|
||||
|
||||
/// A message takes longer to transmit than the engine is given to make
|
||||
/// progress, and the two are not the same thing. Measured as one, the key
|
||||
/// went down partway through a CQ.
|
||||
[Fact]
|
||||
public async Task TheKeyWaitsForTheWholeMessageAndNotForTheStopPatience()
|
||||
{
|
||||
const string message = "CQ CQ DE OM5M OM5M K";
|
||||
FakeEngine engine = new();
|
||||
using DigitalEngineSender sender = new(engine, baud: Slow);
|
||||
sender.Transmit();
|
||||
|
||||
await sender.SendAsync(message);
|
||||
DateTime from = DateTime.UtcNow;
|
||||
sender.ReturnToReceiveWhenSent();
|
||||
|
||||
await WaitForAsync(() => engine.Released);
|
||||
TimeSpan waited = DateTime.UtcNow - from;
|
||||
Assert.Equal(message, engine.Sent);
|
||||
Assert.True(
|
||||
waited > DigitalEngineSender.StopPatience,
|
||||
$"the key went down after {waited.TotalMilliseconds:0} ms, before the message was out");
|
||||
}
|
||||
|
||||
/// Transmitting again while the last message is still ending. The ending
|
||||
/// takes as long as the engine takes to transmit what it holds, and the
|
||||
/// operator can key inside that time; the old ending must not put the key
|
||||
/// down in the middle of the new message.
|
||||
[Fact]
|
||||
public async Task TransmittingAgainAbandonsTheEndingOfTheMessageBeforeIt()
|
||||
{
|
||||
FakeEngine engine = new();
|
||||
using DigitalEngineSender sender = new(engine, baud: Slow);
|
||||
sender.Transmit();
|
||||
|
||||
await sender.SendAsync("CQ");
|
||||
sender.ReturnToReceiveWhenSent();
|
||||
await WaitForAsync(() => engine.Stopped);
|
||||
|
||||
sender.Transmit();
|
||||
await sender.SendAsync("TU");
|
||||
await WaitForAsync(() => engine.Sent == "CQTU");
|
||||
|
||||
Assert.Equal("CQTU", engine.Sent);
|
||||
Assert.False(engine.Released, "the old ending put the key down during the new message");
|
||||
Assert.True(engine.IsTransmitting);
|
||||
}
|
||||
|
||||
/// The same through a message alone, with no `{TX}` in front of it: text is
|
||||
/// the operator asking for the transmitter.
|
||||
[Fact]
|
||||
public async Task AMessageDuringAnEndingKeysAgainAndGoesOut()
|
||||
{
|
||||
FakeEngine engine = new();
|
||||
using DigitalEngineSender sender = new(engine, baud: Slow);
|
||||
sender.Transmit();
|
||||
|
||||
await sender.SendAsync("CQ");
|
||||
sender.ReturnToReceiveWhenSent();
|
||||
await WaitForAsync(() => engine.Stopped);
|
||||
int keyed = engine.Keyed;
|
||||
|
||||
await sender.SendAsync("TU");
|
||||
|
||||
await WaitForAsync(() => engine.Sent == "CQTU");
|
||||
Assert.Equal("CQTU", engine.Sent);
|
||||
Assert.True(engine.Keyed > keyed, "the transmitter was not keyed for the new message");
|
||||
Assert.False(engine.Released);
|
||||
}
|
||||
|
||||
/// The engine dropping ends the message it was ending, and nothing else.
|
||||
[Fact]
|
||||
public async Task TheKeyStaysDownAfterAMessageHasEnded()
|
||||
{
|
||||
FakeEngine engine = new();
|
||||
using DigitalEngineSender sender = new(engine, baud: Fast);
|
||||
sender.Transmit();
|
||||
|
||||
await sender.SendAsync("CQ TEST");
|
||||
sender.ReturnToReceiveWhenSent();
|
||||
await WaitForAsync(() => engine.Stopped);
|
||||
engine.Drop();
|
||||
|
||||
await WaitForAsync(() => !sender.Buffer.IsTransmitting);
|
||||
int keyed = engine.Keyed;
|
||||
await Task.Delay(500);
|
||||
Assert.Equal(keyed, engine.Keyed);
|
||||
Assert.False(engine.IsTransmitting);
|
||||
}
|
||||
|
||||
/// A macro pressed just as the last one finishes. The stop for that message
|
||||
/// is inside the engine, waiting for its buffer to empty, and feeding an
|
||||
/// engine with that standing left MMTTY keyed and transmitting nothing. The
|
||||
/// stop is cleared before the new message is fed.
|
||||
[Fact]
|
||||
public async Task AMessageStartedWhileTheLastOneIsEndingClearsTheStopFirst()
|
||||
{
|
||||
FakeEngine engine = new();
|
||||
using DigitalEngineSender sender = new(engine, baud: Fast);
|
||||
sender.Transmit();
|
||||
await sender.SendAsync("CQ");
|
||||
sender.ReturnToReceiveWhenSent();
|
||||
await WaitForAsync(() => engine.Stopped);
|
||||
|
||||
await sender.SendAsync("TU");
|
||||
|
||||
await WaitForAsync(() => engine.Sent == "CQTU");
|
||||
Assert.Equal("CQTU", engine.Sent);
|
||||
Assert.True(engine.Aborted, "the engine was fed with its stop still pending");
|
||||
Assert.True(engine.IsTransmitting);
|
||||
}
|
||||
|
||||
/// Two macros pressed one after the other keep the transmitter up, so the
|
||||
/// engine never reports the drop that ends a message. The pane still starts
|
||||
/// again on each of them. Text that has gone to the engine cannot be
|
||||
/// edited, and keeping the whole run in the pane left none of it editable.
|
||||
[Fact]
|
||||
public async Task AMessageStartedAfterTheLastOneLeavesOnlyItsOwnTextInThePane()
|
||||
{
|
||||
FakeEngine engine = new();
|
||||
using DigitalEngineSender sender = new(engine, baud: Fast);
|
||||
sender.Transmit();
|
||||
await sender.SendAsync("CQ");
|
||||
sender.ReturnToReceiveWhenSent();
|
||||
await WaitForAsync(() => engine.Stopped);
|
||||
|
||||
await sender.SendAsync("TU");
|
||||
|
||||
await WaitForAsync(() => engine.Sent == "CQTU");
|
||||
Assert.Equal("TU", sender.Buffer.Sent + sender.Buffer.Pending);
|
||||
}
|
||||
|
||||
private static async Task WaitForAsync(Func<bool> ready)
|
||||
{
|
||||
DateTime giveUp = DateTime.UtcNow + Patience;
|
||||
while (!ready() && DateTime.UtcNow < giveUp)
|
||||
{
|
||||
await Task.Delay(2);
|
||||
}
|
||||
}
|
||||
|
||||
/// An engine with no buffer of its own: it takes what it is given, says
|
||||
/// when it was told to stop, and drops the transmitter when the test says
|
||||
/// so.
|
||||
private sealed class FakeEngine : DigitalEngine
|
||||
{
|
||||
private readonly StringBuilder sent = new();
|
||||
private readonly List<string> pushes = [];
|
||||
private readonly Lock gate = new();
|
||||
|
||||
public bool IsConnected => true;
|
||||
|
||||
public bool IsTransmitting { get; private set; } = true;
|
||||
|
||||
/// True once `{RX}` told the engine to stop.
|
||||
public bool Stopped { get; private set; }
|
||||
|
||||
/// True once the engine was stopped the hard way.
|
||||
public bool Aborted { get; private set; }
|
||||
|
||||
/// True once the key was put down, which is what ends a message on
|
||||
/// MMTTY.
|
||||
public bool Released { get; private set; }
|
||||
|
||||
/// How many times the transmitter has been keyed.
|
||||
public int Keyed { get; private set; }
|
||||
|
||||
public string Sent
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (gate)
|
||||
{
|
||||
return sent.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Every call, so a test can tell a message pushed in one piece from
|
||||
/// the same text fed one character at a time.
|
||||
public IReadOnlyList<string> Pushes
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (gate)
|
||||
{
|
||||
return [.. pushes];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// nothing here decodes or disconnects, so these two are declared to
|
||||
// satisfy the interface and never raised
|
||||
public event EventHandler<string>? Received
|
||||
{
|
||||
add { }
|
||||
remove { }
|
||||
}
|
||||
|
||||
public event EventHandler<bool>? TransmitChanged;
|
||||
|
||||
public event EventHandler<bool>? ConnectionChanged
|
||||
{
|
||||
add { }
|
||||
remove { }
|
||||
}
|
||||
|
||||
/// The engine reporting the transmitter down.
|
||||
public void Drop()
|
||||
{
|
||||
IsTransmitting = false;
|
||||
TransmitChanged?.Invoke(this, false);
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellation = default) => Task.CompletedTask;
|
||||
|
||||
public Task KeyAsync(CancellationToken cancellation = default)
|
||||
{
|
||||
Keyed++;
|
||||
Stopped = false;
|
||||
IsTransmitting = true;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SendAsync(string text, CancellationToken cancellation = default)
|
||||
{
|
||||
lock (gate)
|
||||
{
|
||||
sent.Append(text);
|
||||
pushes.Add(text);
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task AbortAsync(CancellationToken cancellation = default)
|
||||
{
|
||||
Aborted = true;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task ReturnToReceiveAsync(CancellationToken cancellation = default)
|
||||
{
|
||||
Stopped = true;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// MMTTY drops the transmitter as soon as the key goes down, and this
|
||||
/// does the same.
|
||||
public Task ReleaseKeyAsync(CancellationToken cancellation = default)
|
||||
{
|
||||
Released = true;
|
||||
Drop();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,20 +3,20 @@ 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.
|
||||
/// The text waiting to go out, fed to the engine one character per character
|
||||
/// time. The feeder 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;
|
||||
/// test can still catch the feeder partway through.
|
||||
private const double Fast = 1500;
|
||||
|
||||
/// 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;
|
||||
/// A quarter of a second per character, which is long enough to read what
|
||||
/// the engine is holding before it has transmitted it.
|
||||
private const double Slow = 30;
|
||||
|
||||
private readonly StringBuilder went = new();
|
||||
|
||||
@@ -31,6 +31,7 @@ public class TypeAheadTests
|
||||
}
|
||||
}
|
||||
|
||||
/// An engine with no count of its own, so the clock alone paces it.
|
||||
private TypeAhead Buffer(double baud = Fast) =>
|
||||
new(
|
||||
(character, _) =>
|
||||
@@ -68,7 +69,7 @@ public class TypeAheadTests
|
||||
[Fact]
|
||||
public async Task WhatHasGoneOutIsNotPendingAnyMore()
|
||||
{
|
||||
using TypeAhead buffer = Buffer(baud: 40);
|
||||
using TypeAhead buffer = Buffer();
|
||||
|
||||
buffer.Append("CQ TEST DE OM5M");
|
||||
|
||||
@@ -77,52 +78,72 @@ public class TypeAheadTests
|
||||
Assert.Equal("CQ TEST DE OM5M", buffer.Sent + buffer.Pending);
|
||||
}
|
||||
|
||||
/// The pane is edited as a whole: what has gone out and what is still to
|
||||
/// go. What has gone out is dropped, and the rest replaces what was
|
||||
/// waiting.
|
||||
[Fact]
|
||||
public async Task TheOperatorRewritesWhatHasNotGoneOut()
|
||||
{
|
||||
using TypeAhead buffer = Buffer(baud: 60);
|
||||
using TypeAhead buffer = Buffer();
|
||||
buffer.Append("OM5X 599 001");
|
||||
|
||||
await WaitForAsync(() => buffer.Sent.Length >= 5);
|
||||
buffer.Rewrite("599 002", TypeAhead.NoCursor);
|
||||
|
||||
buffer.Edit(buffer.Sent + "599 002");
|
||||
|
||||
await WaitForAsync(() => buffer.Pending.Length == 0);
|
||||
Assert.EndsWith("599 002", Sent, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("001", Sent, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// What is typed stays off the air until the transmitter is keyed.
|
||||
[Fact]
|
||||
public async Task NothingGoesOutFromBehindTheCursor()
|
||||
public async Task NothingTypedGoesOutBeforeTheTransmitterIsKeyed()
|
||||
{
|
||||
using TypeAhead buffer = Buffer();
|
||||
|
||||
buffer.Rewrite("CQ TEST", 2);
|
||||
buffer.Edit("CQ TEST");
|
||||
|
||||
await WaitForAsync(() => Sent.Length == 2);
|
||||
await Task.Delay(50);
|
||||
Assert.Equal("CQ", Sent);
|
||||
Assert.Equal(" TEST", buffer.Pending);
|
||||
Assert.Equal("", Sent);
|
||||
Assert.Equal("CQ TEST", buffer.Pending);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TheRestGoesOutWhenTheCursorMovesOn()
|
||||
public async Task TheWholePaneGoesOutOnTransmit()
|
||||
{
|
||||
using TypeAhead buffer = Buffer();
|
||||
buffer.Rewrite("CQ TEST", 2);
|
||||
await WaitForAsync(() => Sent.Length == 2);
|
||||
buffer.Edit("CQ TEST");
|
||||
await Task.Delay(20);
|
||||
|
||||
buffer.Cursor = TypeAhead.NoCursor;
|
||||
buffer.Transmit();
|
||||
|
||||
await WaitForAsync(() => Sent == "CQ TEST");
|
||||
Assert.Equal("CQ TEST", Sent);
|
||||
}
|
||||
|
||||
/// The workflow the pane is for: keyed up, what is typed goes out behind
|
||||
/// what is already going, with no second key to press.
|
||||
[Fact]
|
||||
public async Task TextAddedBehindTheCursorStillGoesOut()
|
||||
public async Task TextTypedWhileTransmittingGoesOutBehindIt()
|
||||
{
|
||||
using TypeAhead buffer = Buffer();
|
||||
buffer.Rewrite("CQ", 2);
|
||||
await WaitForAsync(() => Sent == "CQ");
|
||||
buffer.Edit("CQ ");
|
||||
buffer.Transmit();
|
||||
await WaitForAsync(() => Sent == "CQ ");
|
||||
|
||||
buffer.Edit(buffer.Sent + "DE OM5M");
|
||||
|
||||
await WaitForAsync(() => Sent == "CQ DE OM5M");
|
||||
Assert.Equal("CQ DE OM5M", Sent);
|
||||
}
|
||||
|
||||
/// A function key pressed while the operator is typing goes on the end of
|
||||
/// what has been typed, and keys the transmitter itself.
|
||||
[Fact]
|
||||
public async Task AMacroFollowsWhatWasTypedAndKeysTheTransmitter()
|
||||
{
|
||||
using TypeAhead buffer = Buffer();
|
||||
buffer.Edit("CQ");
|
||||
|
||||
buffer.Append(" TEST");
|
||||
|
||||
@@ -130,12 +151,200 @@ public class TypeAheadTests
|
||||
Assert.Equal("CQ TEST", Sent);
|
||||
}
|
||||
|
||||
/// Text taken out of the pane before it goes out is never transmitted.
|
||||
[Fact]
|
||||
public async Task TextDeletedBeforeItGoesOutIsNotSent()
|
||||
{
|
||||
using TypeAhead buffer = Buffer();
|
||||
buffer.Edit("CQ TEST DE OM5X");
|
||||
buffer.Transmit();
|
||||
await WaitForAsync(() => buffer.Sent.Length >= 3);
|
||||
|
||||
buffer.Edit(buffer.Sent + "DE OM5M");
|
||||
|
||||
await WaitForAsync(() => buffer.Pending.Length == 0);
|
||||
Assert.EndsWith("DE OM5M", Sent, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("OM5X", Sent, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// The transmitter dropping ends the message: what went out is cleared off
|
||||
/// the pane and nothing goes out again until the transmitter is keyed.
|
||||
[Fact]
|
||||
public async Task TheEndOfAMessageClearsWhatWentOutAndShutsTheGate()
|
||||
{
|
||||
using TypeAhead buffer = Buffer();
|
||||
buffer.Append("TU");
|
||||
await WaitForAsync(() => Sent == "TU");
|
||||
|
||||
buffer.Ended();
|
||||
buffer.Edit(" NEXT");
|
||||
|
||||
Assert.Equal("", buffer.Sent);
|
||||
Assert.Equal(" NEXT", buffer.Pending);
|
||||
await Task.Delay(50);
|
||||
Assert.Equal("TU", Sent);
|
||||
}
|
||||
|
||||
/// `Aired` is the end of the message: the engine has been given
|
||||
/// everything and has transmitted it too.
|
||||
[Fact]
|
||||
public async Task DrainedComesWhenTheMessageHasGoneOut()
|
||||
{
|
||||
using TypeAhead buffer = Buffer();
|
||||
int drained = 0;
|
||||
buffer.Aired += (_, _) => Interlocked.Increment(ref drained);
|
||||
buffer.Append("CQ TEST DE OM5M");
|
||||
|
||||
Assert.Equal(0, Volatile.Read(ref drained));
|
||||
await WaitForAsync(() => Volatile.Read(ref drained) == 1);
|
||||
Assert.Equal("CQ TEST DE OM5M", Sent);
|
||||
}
|
||||
|
||||
/// The red text in the pane is `OnAir`, and it ran ahead of the
|
||||
/// transmission because it advanced one character per symbol time. A digit
|
||||
/// in a callsign costs a shift to figures and the letter after it a shift
|
||||
/// back, so `OM5M` is six symbols and not four.
|
||||
[Fact]
|
||||
public async Task WhatIsOnTheAirIsPricedInSymbolsAndNotInCharacters()
|
||||
{
|
||||
using TypeAhead buffer = Buffer();
|
||||
buffer.Append("OM5M");
|
||||
|
||||
// four symbol times is what the old clock allowed the whole callsign
|
||||
await WaitForAsync(() => buffer.Sent.Length == 4);
|
||||
await Task.Delay(buffer.SymbolTime * 4.5);
|
||||
|
||||
Assert.True(
|
||||
buffer.OnAir < 4,
|
||||
$"{buffer.OnAir} of 4 characters were called transmitted in four symbol times");
|
||||
}
|
||||
|
||||
/// Letters with nothing to shift for cost one symbol each, so the two
|
||||
/// agree there.
|
||||
[Fact]
|
||||
public async Task PlainLettersGoOutAtOneSymbolEach()
|
||||
{
|
||||
using TypeAhead buffer = Buffer();
|
||||
buffer.Append("CQ TEST");
|
||||
|
||||
await WaitForAsync(() => buffer.OnAir >= 4);
|
||||
Assert.True(buffer.OnAir <= 7);
|
||||
}
|
||||
|
||||
/// The engine's count is what says how much is left, whatever the clock
|
||||
/// thinks. An engine that says it is still holding the whole message keeps
|
||||
/// the pane from marking any of it as gone out.
|
||||
[Fact]
|
||||
public async Task TheEnginesOwnCountHoldsThePaneBack()
|
||||
{
|
||||
FakeEngine engine = new() { Answer = 30 };
|
||||
using TypeAhead buffer = new(engine, Fast);
|
||||
buffer.Append("CQ TEST DE OM5M");
|
||||
|
||||
await WaitForAsync(() => buffer.Sent.Length == 15);
|
||||
await Task.Delay(buffer.SymbolTime * 20);
|
||||
|
||||
Assert.Equal(0, buffer.OnAir);
|
||||
}
|
||||
|
||||
/// And it lets go as the count falls.
|
||||
[Fact]
|
||||
public async Task ThePaneCatchesUpAsTheCountFalls()
|
||||
{
|
||||
FakeEngine engine = new() { Answer = 30 };
|
||||
using TypeAhead buffer = new(engine, Fast);
|
||||
buffer.Append("CQ TEST DE OM5M");
|
||||
await WaitForAsync(() => buffer.Sent.Length == 15);
|
||||
|
||||
engine.Answer = 0;
|
||||
|
||||
await WaitForAsync(() => buffer.OnAir == 15);
|
||||
Assert.Equal(15, buffer.OnAir);
|
||||
}
|
||||
|
||||
/// The engine's count is what says how much has gone out. It answers in
|
||||
/// symbols and they fall as they are transmitted, so what it drops between
|
||||
/// two answers is what went on the air between them.
|
||||
[Fact]
|
||||
public async Task WhatTheEngineHasTransmittedComesFromItsOwnCount()
|
||||
{
|
||||
FakeEngine engine = new() { Answer = 20 };
|
||||
using TypeAhead buffer = new(engine, Fast);
|
||||
buffer.Append("CQ TEST DE OM5M");
|
||||
await WaitForAsync(() => buffer.Sent.Length == 15);
|
||||
await WaitForAsync(() => buffer.EngineHolds == 20);
|
||||
|
||||
// four symbols out of the engine, which is the first four characters
|
||||
engine.Answer = 16;
|
||||
|
||||
await WaitForAsync(() => buffer.OnAir == 4);
|
||||
Assert.Equal(4, buffer.OnAir);
|
||||
}
|
||||
|
||||
/// A count that has not moved leaves the pane where it is, however long
|
||||
/// the clock runs.
|
||||
[Fact]
|
||||
public async Task ACountThatDoesNotMoveHoldsThePaneStill()
|
||||
{
|
||||
FakeEngine engine = new() { Answer = 20 };
|
||||
using TypeAhead buffer = new(engine, Fast);
|
||||
buffer.Append("CQ TEST DE OM5M");
|
||||
await WaitForAsync(() => buffer.Sent.Length == 15);
|
||||
|
||||
await Task.Delay(buffer.SymbolTime * 20);
|
||||
|
||||
Assert.Equal(0, buffer.OnAir);
|
||||
}
|
||||
|
||||
/// An empty engine has transmitted everything it was given, whatever the
|
||||
/// symbols added up to along the way. A wrong guess about the shift
|
||||
/// corrects itself at the end of every message rather than accumulating.
|
||||
[Fact]
|
||||
public async Task AnEmptyEngineHasTransmittedEverything()
|
||||
{
|
||||
FakeEngine engine = new() { Answer = 20 };
|
||||
using TypeAhead buffer = new(engine, Fast);
|
||||
buffer.Append("CQ TEST DE OM5M");
|
||||
await WaitForAsync(() => buffer.Sent.Length == 15);
|
||||
await WaitForAsync(() => buffer.EngineHolds == 20);
|
||||
|
||||
engine.Answer = 0;
|
||||
|
||||
await WaitForAsync(() => buffer.OnAir == 15);
|
||||
Assert.Equal(15, buffer.OnAir);
|
||||
}
|
||||
|
||||
/// The count reads 0 for the first 150 ms after a push, before the engine
|
||||
/// has caught up with what it was given. That 0 does not mean the message
|
||||
/// is over.
|
||||
[Fact]
|
||||
public async Task ACountThatHasNotCaughtUpIsNotAnEmptyEngine()
|
||||
{
|
||||
FakeEngine engine = new() { Answer = 0 };
|
||||
using TypeAhead buffer = new(engine, Fast);
|
||||
buffer.Append("CQ TEST DE OM5M");
|
||||
|
||||
await WaitForAsync(() => buffer.Sent.Length == 15);
|
||||
await Task.Delay(buffer.SymbolTime * 8);
|
||||
|
||||
Assert.Equal(0, buffer.OnAir);
|
||||
}
|
||||
|
||||
/// Nothing has been given to the engine, so nothing is outstanding.
|
||||
[Fact]
|
||||
public void NothingIsOutstandingWhenNothingIsGoingOut()
|
||||
{
|
||||
using TypeAhead buffer = Buffer();
|
||||
|
||||
Assert.Equal(0, buffer.Outstanding);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TheBufferSaysWhenTheMessageHasGoneOut()
|
||||
{
|
||||
using TypeAhead buffer = Buffer();
|
||||
int drained = 0;
|
||||
buffer.Drained += (_, _) => Interlocked.Increment(ref drained);
|
||||
buffer.Aired += (_, _) => Interlocked.Increment(ref drained);
|
||||
|
||||
buffer.Append("TU");
|
||||
|
||||
@@ -146,7 +355,7 @@ public class TypeAheadTests
|
||||
[Fact]
|
||||
public async Task AMessageAddedWhileOneIsGoingOutFollowsIt()
|
||||
{
|
||||
using TypeAhead buffer = Buffer(baud: 400);
|
||||
using TypeAhead buffer = Buffer();
|
||||
buffer.Append("CQ ");
|
||||
|
||||
buffer.Append("DE OM5M");
|
||||
@@ -158,7 +367,7 @@ public class TypeAheadTests
|
||||
[Fact]
|
||||
public async Task DroppingLeavesWhatHasAlreadyGone()
|
||||
{
|
||||
using TypeAhead buffer = Buffer(baud: 60);
|
||||
using TypeAhead buffer = Buffer();
|
||||
buffer.Append("CQ TEST DE OM5M");
|
||||
await WaitForAsync(() => buffer.Sent.Length >= 3);
|
||||
|
||||
@@ -174,7 +383,7 @@ public class TypeAheadTests
|
||||
[Fact]
|
||||
public void ClearingEmptiesBothHalves()
|
||||
{
|
||||
using TypeAhead buffer = Buffer(baud: 1);
|
||||
using TypeAhead buffer = Buffer();
|
||||
buffer.Append("CQ TEST");
|
||||
|
||||
buffer.Clear();
|
||||
@@ -183,63 +392,71 @@ public class TypeAheadTests
|
||||
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.
|
||||
/// MMTTY answers 0 while it is still transmitting: for the first 150 ms
|
||||
/// after a push, and for as long as it holds a word on Word out. A feeder
|
||||
/// that read that as room fed on it and ran ahead of the air, so the clock
|
||||
/// is the pace and the count only ever holds it back. This one runs at the
|
||||
/// real RTTY speed, because the number that matters is how many characters
|
||||
/// go out in a second at 45.45 baud.
|
||||
[Fact]
|
||||
public async Task TheEngineIsKeptOneCharacterAhead()
|
||||
public async Task AnEngineAnsweringZeroDoesNotPullThePumpForward()
|
||||
{
|
||||
using TypeAhead buffer = Buffer(baud: Slow);
|
||||
FakeEngine engine = new() { Answer = 0 };
|
||||
using TypeAhead buffer = new(engine, baud: TypeAhead.DefaultBaud);
|
||||
|
||||
buffer.Append("CQ TEST");
|
||||
buffer.Append("CQ TEST DE OM5M OM5M K");
|
||||
|
||||
await WaitForAsync(() => Sent.Length >= 2);
|
||||
Assert.Equal("CQ", Sent);
|
||||
await Task.Delay(TimeSpan.FromSeconds(1));
|
||||
// 45.45 baud is 6.06 characters a second, plus the `Ahead` characters
|
||||
// the engine is primed with and one for the poll granularity
|
||||
Assert.InRange(engine.Waiting.Length, 7, 9);
|
||||
}
|
||||
|
||||
/// Only the lead goes out ahead. The rest waits, which is what leaves it
|
||||
/// where the operator can still change it.
|
||||
/// The engine is given the next character before it has transmitted the one
|
||||
/// on the air, so it has one in hand when that one finishes. An engine left
|
||||
/// with an empty buffer transmits the idle tone instead, which is audible
|
||||
/// between the characters of a long word.
|
||||
[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 engine's count of what it has left runs behind what it is really
|
||||
/// doing: the probe read 0 fifty milliseconds after twenty-one characters
|
||||
/// had been pushed and were already going out. A pump that believed that
|
||||
/// would hand over the whole message at once, so the count only ever stops
|
||||
/// it, and the engine is never given more than `Lead` plus `Slack`.
|
||||
[Fact]
|
||||
public async Task AnEngineBehindTheClockIsNotFedPastTheCap()
|
||||
public async Task TheEngineIsNeverLeftWithNothingInHand()
|
||||
{
|
||||
FakeEngine engine = new();
|
||||
using TypeAhead buffer = new(engine, baud: Slow);
|
||||
// half the speed of the baud rate the pump is paced by
|
||||
using Timer transmitting = new(_ => engine.Transmit(1), null, 0, 200);
|
||||
using TypeAhead buffer = new(engine, baud: TypeAhead.DefaultBaud);
|
||||
// transmits one character every character time, as a real engine does
|
||||
using Timer transmitting = new(_ => engine.Transmit(1), null, 165, 165);
|
||||
|
||||
buffer.Append(new string('N', 30));
|
||||
|
||||
int emptied = 0;
|
||||
for (int look = 0; look < 60; look++)
|
||||
{
|
||||
await Task.Delay(30);
|
||||
if (engine.Waiting.Length == 0 && buffer.Pending.Length > 0)
|
||||
{
|
||||
emptied++;
|
||||
}
|
||||
}
|
||||
Assert.Equal(0, emptied);
|
||||
}
|
||||
|
||||
/// The engine transmitting makes room, and the feeder fills it. Nothing
|
||||
/// here guesses how fast the engine is going: it says, and it is believed.
|
||||
[Fact]
|
||||
public async Task TheEngineIsFedAsItMakesRoom()
|
||||
{
|
||||
FakeEngine engine = new();
|
||||
using TypeAhead buffer = new(engine);
|
||||
using Timer transmitting = new(_ => engine.Transmit(1), null, 0, 10);
|
||||
|
||||
buffer.Append("CQ TEST DE OM5M");
|
||||
|
||||
int most = 0;
|
||||
for (int look = 0; look < 50; look++)
|
||||
{
|
||||
most = Math.Max(most, engine.Waiting.Length);
|
||||
await Task.Delay(20);
|
||||
}
|
||||
Assert.True(most <= TypeAhead.DefaultLead + TypeAhead.Slack, $"the engine was given {most}");
|
||||
Assert.True(engine.Transmitted.Length > 0, "nothing was transmitted at all");
|
||||
await WaitForAsync(() => engine.Transmitted == "CQ TEST DE OM5M");
|
||||
Assert.Equal("CQ TEST DE OM5M", engine.Transmitted);
|
||||
}
|
||||
|
||||
/// The clock is the fallback for an engine that will not answer.
|
||||
/// An engine that will not say how much it holds is fed anyway: there is
|
||||
/// nothing to pace against, so it gets the message.
|
||||
[Fact]
|
||||
public async Task AnEngineThatWillNotCountIsPacedByTheClock()
|
||||
public async Task AnEngineThatWillNotCountIsFedAnyway()
|
||||
{
|
||||
FakeEngine engine = new() { Counts = false };
|
||||
using TypeAhead buffer = new(engine, baud: Fast);
|
||||
@@ -252,24 +469,49 @@ public class TypeAheadTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ACharacterTakesAsLongAsTheBaudRateSays()
|
||||
public async Task WhatTheEngineIsStillHoldingHasNotGoneOutYet()
|
||||
{
|
||||
using TypeAhead buffer = Buffer(baud: TypeAhead.DefaultBaud);
|
||||
using TypeAhead buffer = Buffer(Slow);
|
||||
|
||||
Assert.Equal(165, buffer.CharacterTime.TotalMilliseconds, 0.5);
|
||||
buffer.Append("AB");
|
||||
|
||||
await WaitForAsync(() => buffer.Sent == "AB");
|
||||
Assert.Equal(0, buffer.OnAir);
|
||||
await WaitForAsync(() => buffer.OnAir == 2);
|
||||
Assert.Equal(2, buffer.OnAir);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TheMessageIsNotOverUntilTheEngineHasTransmittedWhatItHolds()
|
||||
{
|
||||
using TypeAhead buffer = Buffer(Slow);
|
||||
int atTheEnd = -1;
|
||||
buffer.Aired += (_, _) => atTheEnd = buffer.OnAir;
|
||||
|
||||
buffer.Append("AB");
|
||||
|
||||
await WaitForAsync(() => atTheEnd >= 0);
|
||||
Assert.Equal(2, atTheEnd);
|
||||
}
|
||||
|
||||
/// An engine with a buffer of its own: it takes characters, holds them
|
||||
/// until the test says they have been transmitted, and says how many it
|
||||
/// has. `Counts` false is the engine that will not answer.
|
||||
/// has. `Counts` false is the engine that will not answer, and `Answer` is
|
||||
/// the engine that answers a number of its own rather than what it holds:
|
||||
/// MMTTY reads 0 while it is still transmitting, and reads high while it is
|
||||
/// holding a word.
|
||||
private sealed class FakeEngine : EngineBuffer
|
||||
{
|
||||
private readonly Lock gate = new();
|
||||
private readonly StringBuilder waiting = new();
|
||||
private readonly StringBuilder transmitted = new();
|
||||
|
||||
public bool Counts { get; init; } = true;
|
||||
|
||||
/// What to answer instead of what is really waiting, or -1 to answer
|
||||
/// what is waiting. A test can move it while the buffer is running, the
|
||||
/// way a real engine's count falls as it transmits.
|
||||
public int Answer { get; set; } = -1;
|
||||
|
||||
public event EventHandler<int>? Buffered;
|
||||
|
||||
public string Waiting
|
||||
@@ -308,7 +550,7 @@ public class TypeAheadTests
|
||||
int left;
|
||||
lock (gate)
|
||||
{
|
||||
left = Counts ? waiting.Length : -1;
|
||||
left = Counts ? (Answer >= 0 ? Answer : waiting.Length) : -1;
|
||||
}
|
||||
Buffered?.Invoke(this, left);
|
||||
return Task.CompletedTask;
|
||||
|
||||
Reference in New Issue
Block a user