Alternating CQ ran off every message the keyer finished, so working a station that answered started a CQ on the other radio, and Escape -- which stopped it outright -- was the way around that. It now follows N1MM: only the CQ message carries the alternation to the other radio, anything else holds it where it is, and so does Escape. The next CQ picks it up again, which is what the CQ after a logged contact does, so Ctrl+B is needed once rather than after every contact. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
175 lines
4.8 KiB
C#
175 lines
4.8 KiB
C#
using Nonemm.Keying;
|
|
|
|
namespace Nonemm.Session.Tests;
|
|
|
|
public class AlternatingCqTests
|
|
{
|
|
/// A keyer that sends nothing and says it has finished when the test says
|
|
/// so, so the alternation is what is under test rather than a timer.
|
|
private sealed class FakeKeyer : MessageSender
|
|
{
|
|
public bool IsReady => true;
|
|
|
|
public bool ReportsCompletion { get; init; } = true;
|
|
|
|
public event EventHandler? Finished;
|
|
|
|
public void FinishMessage() => Finished?.Invoke(this, EventArgs.Empty);
|
|
|
|
public Task SendAsync(string text, CancellationToken cancellation = default) =>
|
|
Task.CompletedTask;
|
|
|
|
public Task AbortAsync(CancellationToken cancellation = default) => Task.CompletedTask;
|
|
|
|
public Task SetSpeedAsync(int wordsPerMinute, CancellationToken cancellation = default) =>
|
|
Task.CompletedTask;
|
|
|
|
public void Dispose()
|
|
{
|
|
}
|
|
}
|
|
|
|
private static readonly Func<TimeSpan, CancellationToken, Task> NoWait =
|
|
(_, _) => Task.CompletedTask;
|
|
|
|
private static AlternatingCq Driving(FakeKeyer keyer, List<int> called) =>
|
|
new(
|
|
keyer,
|
|
radio =>
|
|
{
|
|
called.Add(radio);
|
|
return Task.CompletedTask;
|
|
},
|
|
TimeSpan.Zero,
|
|
NoWait);
|
|
|
|
[Fact]
|
|
public void StartingCallsOnTheRadioItWasGiven()
|
|
{
|
|
FakeKeyer keyer = new();
|
|
List<int> called = [];
|
|
using AlternatingCq cq = Driving(keyer, called);
|
|
|
|
cq.Start(2);
|
|
|
|
Assert.Equal([2], called);
|
|
Assert.True(cq.IsRunning);
|
|
Assert.Equal(2, cq.RadioNumber);
|
|
}
|
|
|
|
[Fact]
|
|
public void TheOtherRadioCallsOnceTheMessageHasGoneOut()
|
|
{
|
|
FakeKeyer keyer = new();
|
|
List<int> called = [];
|
|
using AlternatingCq cq = Driving(keyer, called);
|
|
|
|
cq.Start(1);
|
|
keyer.FinishMessage();
|
|
keyer.FinishMessage();
|
|
keyer.FinishMessage();
|
|
|
|
Assert.Equal([1, 2, 1, 2], called);
|
|
}
|
|
|
|
[Fact]
|
|
public void StoppingLeavesTheKeyerAlone()
|
|
{
|
|
FakeKeyer keyer = new();
|
|
List<int> called = [];
|
|
using AlternatingCq cq = Driving(keyer, called);
|
|
|
|
cq.Start(1);
|
|
cq.Stop();
|
|
keyer.FinishMessage();
|
|
|
|
Assert.Equal([1], called);
|
|
Assert.False(cq.IsRunning);
|
|
}
|
|
|
|
/// The message that was already going out when the operator stopped still
|
|
/// reports itself finished, and that must not start the other radio.
|
|
[Fact]
|
|
public void AMessageFinishingAfterAStopStartsNothing()
|
|
{
|
|
FakeKeyer keyer = new();
|
|
List<int> called = [];
|
|
using AlternatingCq cq = Driving(keyer, called);
|
|
|
|
cq.Start(1);
|
|
keyer.FinishMessage();
|
|
cq.Stop();
|
|
keyer.FinishMessage();
|
|
|
|
Assert.Equal([1, 2], called);
|
|
}
|
|
|
|
/// A message that is not a CQ leaves the CQ where it is, and the next CQ
|
|
/// carries the alternation on. This is what happens while a contact is
|
|
/// worked, and it is N1MM's rule.
|
|
[Fact]
|
|
public void WorkingAStationHoldsTheCqOnOneRadio()
|
|
{
|
|
FakeKeyer keyer = new();
|
|
List<int> called = [];
|
|
using AlternatingCq cq = Driving(keyer, called);
|
|
|
|
cq.Start(1);
|
|
cq.Hold();
|
|
keyer.FinishMessage();
|
|
keyer.FinishMessage();
|
|
|
|
Assert.Equal([1], called);
|
|
Assert.True(cq.IsRunning);
|
|
Assert.False(cq.CarriesOn);
|
|
}
|
|
|
|
/// The CQ that follows a logged contact starts the alternation again,
|
|
/// without the operator switching it back on.
|
|
[Fact]
|
|
public void TheNextCqCarriesTheAlternationOnAgain()
|
|
{
|
|
FakeKeyer keyer = new();
|
|
List<int> called = [];
|
|
using AlternatingCq cq = Driving(keyer, called);
|
|
|
|
cq.Start(1);
|
|
cq.Hold();
|
|
keyer.FinishMessage();
|
|
cq.CarryOn();
|
|
keyer.FinishMessage();
|
|
|
|
Assert.Equal([1, 2], called);
|
|
}
|
|
|
|
[Fact]
|
|
public void AKeyerThatCannotReportCompletionCannotDriveIt()
|
|
{
|
|
FakeKeyer keyer = new() { ReportsCompletion = false };
|
|
List<int> called = [];
|
|
using AlternatingCq cq = Driving(keyer, called);
|
|
|
|
Assert.False(cq.IsPossible);
|
|
Assert.Throws<InvalidOperationException>(() => cq.Start(1));
|
|
Assert.Empty(called);
|
|
}
|
|
|
|
[Fact]
|
|
public void AKeyerThatHasGoneAwayStopsTheWholeThing()
|
|
{
|
|
FakeKeyer keyer = new();
|
|
List<string> reasons = [];
|
|
using AlternatingCq cq = new(
|
|
keyer,
|
|
_ => throw new InvalidOperationException("could not reach cwdaemon at 127.0.0.1:6789"),
|
|
TimeSpan.Zero,
|
|
NoWait);
|
|
cq.Stopped += (_, reason) => reasons.Add(reason);
|
|
|
|
cq.Start(1);
|
|
|
|
Assert.False(cq.IsRunning);
|
|
Assert.Equal(["could not reach cwdaemon at 127.0.0.1:6789"], reasons);
|
|
}
|
|
}
|