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 NoWait = (_, _) => Task.CompletedTask; private static AlternatingCq Driving(FakeKeyer keyer, List called) => new( keyer, radio => { called.Add(radio); return Task.CompletedTask; }, TimeSpan.Zero, NoWait); [Fact] public void StartingCallsOnTheRadioItWasGiven() { FakeKeyer keyer = new(); List 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 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 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 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 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 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 called = []; using AlternatingCq cq = Driving(keyer, called); Assert.False(cq.IsPossible); Assert.Throws(() => cq.Start(1)); Assert.Empty(called); } [Fact] public void AKeyerThatHasGoneAwayStopsTheWholeThing() { FakeKeyer keyer = new(); List 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); } }