Split. RadioState carries a transmit frequency, zero meaning the radio transmits where it listens, so split is not a second flag that can disagree with it. The contact stores it in N1MM's QSX column, the entry window shows 14008.00 followed by the transmit frequency, and the bandmap draws a red bar there. SetSplitAsync sets the frequency before turning split on, or a radio last split somewhere else transmits there. Every command now goes out with a + in front, asking rigctld for its extended answer: named fields ended by an RPRT line. The raw answer is bare values with no terminator, so the client had to know how many lines each command returns — there was a `command == "m"` special case for the one that returns two. Add a third such command and get the count wrong once, and every later answer is read against the wrong command for the rest of the session. RigctldReply parses the extended form and is tested on its own. A radio that cannot do split answers RPRT -11. That is an answer, not a broken connection, so the frequency and mode it did report still count. Two radios. Settings hold a list rather than one host and port, with the single radio an older settings file holds carried into it. Both radios are read and both show on the bandmap, the active one green and the other orange, but only the active one drives the entry window: the second radio moving must not drag the operator off the station being worked. Ctrl+Tab swaps, and a contact records which radio made it. This is not a full two-radio operating position — no second entry window, no alternating CQ, no audio switching. It is two radios read and logged correctly. Tested against a stand-in for rigctld over a real socket, and looked at under Xvfb with two fake radios, one of them split. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
294 lines
9.0 KiB
C#
294 lines
9.0 KiB
C#
using Nonemm.Contests;
|
|
using Nonemm.Contests.Rules;
|
|
using Nonemm.Core;
|
|
using Nonemm.Core.Country;
|
|
using Nonemm.Storage;
|
|
|
|
namespace Nonemm.Session.Tests;
|
|
|
|
public class LoggingSessionTests
|
|
{
|
|
private const string Countries = """
|
|
Germany: 14: 28: EU: 51.00: -10.00: -1.0: DL:
|
|
DL,DK,DJ;
|
|
Japan: 25: 45: AS: 36.40: -138.38: -9.0: JA:
|
|
JA,JH,JR;
|
|
""";
|
|
|
|
private static readonly StationInfo Me = new()
|
|
{
|
|
Callsign = "DL1ABC",
|
|
CqZone = 14,
|
|
ItuZone = 28,
|
|
Continent = "EU",
|
|
CountryPrefix = "DL",
|
|
};
|
|
|
|
private static LoggingSession Session(Contest? contest = null)
|
|
{
|
|
FakeLogStore store = new();
|
|
ContestInstance instance = store.AddContest(new ContestInstance
|
|
{
|
|
ContestNumber = 0,
|
|
ContestName = "CQWW",
|
|
});
|
|
return new LoggingSession(
|
|
store,
|
|
contest ?? new CqWorldWide(ModeCategory.Cw),
|
|
instance,
|
|
Me,
|
|
CountryFile.Parse(Countries));
|
|
}
|
|
|
|
private static void Type(LoggingSession session, string call, string zone)
|
|
{
|
|
session.Entry.Call = call;
|
|
session.Entry.Set(ExchangeSlot.ReceivedReport, "599");
|
|
session.Entry.Set(ExchangeSlot.Zone, zone);
|
|
}
|
|
|
|
[Fact]
|
|
public void LoggingAContactPutsItInTheLogAndClearsTheEntry()
|
|
{
|
|
LoggingSession session = Session();
|
|
Type(session, "JA1XYZ", "25");
|
|
Qso logged = session.LogContact();
|
|
|
|
Assert.Equal("JA1XYZ", logged.Call.Text);
|
|
Assert.Equal(25, logged.Zone);
|
|
Assert.Equal(3, logged.Points);
|
|
Assert.Equal("", session.Entry.Call);
|
|
Assert.Single(session.Log.Qsos);
|
|
}
|
|
|
|
[Fact]
|
|
public void CountryAndPrefixAreFilledInFromTheCountryFile()
|
|
{
|
|
LoggingSession session = Session();
|
|
Type(session, "JA1XYZ", "25");
|
|
Qso logged = session.LogContact();
|
|
|
|
Assert.Equal("JA", logged.CountryPrefix);
|
|
Assert.Equal("AS", logged.Continent);
|
|
Assert.Equal("JA1", logged.WpxPrefix);
|
|
}
|
|
|
|
[Fact]
|
|
public void AnIncompleteExchangeIsNotLogged()
|
|
{
|
|
LoggingSession session = Session();
|
|
session.Entry.Call = "JA1XYZ";
|
|
Assert.Throws<InvalidOperationException>(() => session.LogContact());
|
|
}
|
|
|
|
[Fact]
|
|
public void TheSameStationOnTheSameBandReadsAsADupe()
|
|
{
|
|
LoggingSession session = Session();
|
|
Type(session, "JA1XYZ", "25");
|
|
session.LogContact();
|
|
Type(session, "JA1XYZ", "25");
|
|
Assert.True(session.Verdict()?.IsDupe);
|
|
}
|
|
|
|
[Fact]
|
|
public void MovingBandClearsTheDupe()
|
|
{
|
|
LoggingSession session = Session();
|
|
Type(session, "JA1XYZ", "25");
|
|
session.LogContact();
|
|
session.Tune(Frequency.FromKilohertz(7_025));
|
|
Type(session, "JA1XYZ", "25");
|
|
Assert.False(session.Verdict()?.IsDupe);
|
|
}
|
|
|
|
[Fact]
|
|
public void SerialNumbersCountUp()
|
|
{
|
|
LoggingSession session = Session(new CqWpx(ModeCategory.Cw));
|
|
Assert.Equal(1, session.SentNumber);
|
|
session.Entry.Call = "JA1XYZ";
|
|
session.Entry.Set(ExchangeSlot.ReceivedReport, "599");
|
|
session.Entry.Set(ExchangeSlot.SerialNumber, "12");
|
|
session.LogContact();
|
|
Assert.Equal(2, session.SentNumber);
|
|
}
|
|
|
|
[Fact]
|
|
public void SpaceMovesThroughTheBoxesAndBackToTheCall()
|
|
{
|
|
LoggingSession session = Session();
|
|
Assert.Equal(0, session.Entry.Focus);
|
|
session.Entry.Advance();
|
|
Assert.Equal(1, session.Entry.Focus);
|
|
session.Entry.Advance();
|
|
session.Entry.Advance();
|
|
Assert.Equal(0, session.Entry.Focus);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("14025", 14_025_000)]
|
|
[InlineData("14.025", 14_025_000)]
|
|
[InlineData("7025.5", 7_025_500)]
|
|
public void AFrequencyTypedIntoTheCallBoxIsRecognised(string typed, long hertz)
|
|
{
|
|
LoggingSession session = Session();
|
|
session.Entry.Call = typed;
|
|
Assert.Equal(hertz, session.PendingQsy()?.Hertz);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("JA1XYZ")]
|
|
[InlineData("12000")]
|
|
public void ACallOrAnOutOfBandNumberIsNotAQsy(string typed)
|
|
{
|
|
LoggingSession session = Session();
|
|
session.Entry.Call = typed;
|
|
Assert.Null(session.PendingQsy());
|
|
}
|
|
|
|
[Fact]
|
|
public void AContactDeletedFromTheLogGivesItsMultiplierBack()
|
|
{
|
|
LoggingSession session = Session();
|
|
Type(session, "JA1XYZ", "25");
|
|
Qso first = session.LogContact();
|
|
Type(session, "JA2XYZ", "25");
|
|
session.LogContact();
|
|
Assert.Equal(2, session.Log.Tally.TotalMultipliers);
|
|
|
|
session.Delete(first.Id);
|
|
Assert.Single(session.Log.Qsos);
|
|
Assert.Equal(2, session.Log.Tally.TotalMultipliers);
|
|
Assert.True(session.Log.Qsos.Single().IsMultiplier1);
|
|
}
|
|
|
|
[Fact]
|
|
public void AContactWorkedSplitRecordsWhereWeTransmitted()
|
|
{
|
|
LoggingSession session = Session();
|
|
session.Tune(
|
|
Frequency.FromKilohertz(14_025),
|
|
Modes.Cw,
|
|
Frequency.FromKilohertz(14_200));
|
|
Type(session, "JA1XYZ", "25");
|
|
|
|
Qso logged = session.LogContact();
|
|
|
|
Assert.Equal(14_025_000, logged.Frequency.Hertz);
|
|
Assert.Equal(14_200_000, logged.QsxFrequency.Hertz);
|
|
}
|
|
|
|
[Fact]
|
|
public void AContactRecordsWhichRadioMadeIt()
|
|
{
|
|
LoggingSession session = Session();
|
|
session.RadioNumber = 2;
|
|
Type(session, "JA1XYZ", "25");
|
|
|
|
Assert.Equal(2, session.LogContact().RadioNumber);
|
|
}
|
|
|
|
/// Tuning without saying anything about split leaves it where it was, so a
|
|
/// frequency typed by hand does not clear what the radio reported.
|
|
[Fact]
|
|
public void TuningWithoutASplitLeavesTheOneAlreadySet()
|
|
{
|
|
LoggingSession session = Session();
|
|
session.Tune(Frequency.FromKilohertz(14_025), null, Frequency.FromKilohertz(14_200));
|
|
|
|
session.Tune(Frequency.FromKilohertz(14_030));
|
|
|
|
Assert.Equal(14_200_000, session.TransmitFrequency.Hertz);
|
|
}
|
|
|
|
[Fact]
|
|
public void EditingAContactRescoresTheLog()
|
|
{
|
|
LoggingSession session = Session();
|
|
Type(session, "JA1XYZ", "25");
|
|
Qso logged = session.LogContact();
|
|
Assert.Equal(3, session.Log.Tally.Points);
|
|
|
|
QsoEdit edit = session.Edit(logged.Id, QsoField.Call, "DL2XYZ");
|
|
|
|
Assert.True(edit.IsAccepted);
|
|
Assert.Equal("DL2XYZ", session.Log.Qsos.Single().Call.Text);
|
|
Assert.Equal(0, session.Log.Tally.Points);
|
|
}
|
|
|
|
[Fact]
|
|
public void ARefusedEditLeavesTheLogAlone()
|
|
{
|
|
LoggingSession session = Session();
|
|
Type(session, "JA1XYZ", "25");
|
|
Qso logged = session.LogContact();
|
|
|
|
QsoEdit edit = session.Edit(logged.Id, QsoField.Zone, "41");
|
|
|
|
Assert.False(edit.IsAccepted);
|
|
Assert.Equal(25, session.Log.Qsos.Single().Zone);
|
|
}
|
|
|
|
/// The other stations need the call and time the contact had before, because
|
|
/// that is how they find their own copy of it.
|
|
[Fact]
|
|
public void AnEditReportsWhatTheContactUsedToBe()
|
|
{
|
|
LoggingSession session = Session();
|
|
Type(session, "JA1XYZ", "25");
|
|
Qso logged = session.LogContact();
|
|
QsoChange? change = null;
|
|
session.Edited += (_, c) => change = c;
|
|
|
|
session.Edit(logged.Id, QsoField.Call, "JA2XYZ");
|
|
|
|
Assert.Equal("JA1XYZ", change?.OldCall);
|
|
Assert.Equal(logged.TimestampUtc, change?.OldTimestampUtc);
|
|
Assert.Equal("JA2XYZ", change?.Qso.Call.Text);
|
|
}
|
|
|
|
/// The country file gets a call wrong now and then. Correcting the country
|
|
/// by hand has to change the score, or the correction is decoration.
|
|
[Fact]
|
|
public void CorrectingTheCountryChangesTheScore()
|
|
{
|
|
LoggingSession session = Session();
|
|
Type(session, "JA1XYZ", "25");
|
|
Qso logged = session.LogContact();
|
|
Assert.Equal(3, session.Log.Tally.Points);
|
|
|
|
session.Edit(logged.Id, QsoField.CountryPrefix, "DL");
|
|
|
|
Assert.Equal(0, session.Log.Tally.Points);
|
|
}
|
|
|
|
[Fact]
|
|
public void ADeleteReportsTheContactThatWent()
|
|
{
|
|
LoggingSession session = Session();
|
|
Type(session, "JA1XYZ", "25");
|
|
Qso logged = session.LogContact();
|
|
Qso? gone = null;
|
|
session.Deleted += (_, q) => gone = q;
|
|
|
|
session.Delete(logged.Id);
|
|
|
|
Assert.Equal("JA1XYZ", gone?.Call.Text);
|
|
}
|
|
|
|
[Fact]
|
|
public void StoredContactsAreReadBackWhenTheSessionStarts()
|
|
{
|
|
FakeLogStore store = new();
|
|
ContestInstance instance = store.AddContest(new ContestInstance { ContestNumber = 0, ContestName = "CQWW" });
|
|
LoggingSession first = new(store, new CqWorldWide(ModeCategory.Cw), instance, Me, CountryFile.Parse(Countries));
|
|
Type(first, "JA1XYZ", "25");
|
|
first.LogContact();
|
|
|
|
LoggingSession second = new(store, new CqWorldWide(ModeCategory.Cw), instance, Me, CountryFile.Parse(Countries));
|
|
Assert.Single(second.Log.Qsos);
|
|
Assert.Equal(3, second.Log.Tally.Points);
|
|
}
|
|
}
|