Files
Nonemm/tests/Nonemm.Session.Tests/RadioPositionTests.cs
ericek111 7c2a2ed768 Add the name, comment and lights from N1MM's entry window
Three more things off the screenshot of N1MM's entry window.

A Name box and a Comment box beside the exchange. Neither is part of the
exchange, so an empty one does not hold up a contact and ESM does not wait for
it; both fill the log columns of those names, and a contest that exchanges a
name keeps what was exchanged. Tab now walks the callsign, the exchange, the
name and the comment before coming round again, which is what the boxes being
there implies. The name is filled from the call history file, and what that file
says in its UserText column is written under the entry boxes, where N1MM writes
it.

Two lights beside the run switch: green while a radio is answering, red while
something is going out on the air. The transmit light follows the keyer, so with
cwdaemon it goes out when the message has actually finished rather than when the
text was handed over.

The window is wider to fit the two new boxes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 09:13:58 +00:00

417 lines
13 KiB
C#

using Nonemm.Contests;
using Nonemm.Contests.Rules;
using Nonemm.Core;
using Nonemm.Core.Calls;
using Nonemm.Core.Country;
using Nonemm.Storage;
namespace Nonemm.Session.Tests;
public class RadioPositionTests
{
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 RadioPosition Session(Contest? contest = null) =>
new(Shared(new FakeLogStore(), contest));
private static ContestSession Shared(FakeLogStore store, Contest? contest = null)
{
ContestInstance instance = store.AddContest(new ContestInstance
{
ContestNumber = 0,
ContestName = "CQWW",
});
return new ContestSession(
store,
contest ?? new CqWorldWide(ModeCategory.Cw),
instance,
Me,
CountryFile.Parse(Countries));
}
private static void Type(RadioPosition 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()
{
RadioPosition 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()
{
RadioPosition 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()
{
RadioPosition session = Session();
session.Entry.Call = "JA1XYZ";
Assert.Throws<InvalidOperationException>(() => session.LogContact());
}
[Fact]
public void TheSameStationOnTheSameBandReadsAsADupe()
{
RadioPosition session = Session();
Type(session, "JA1XYZ", "25");
session.LogContact();
Type(session, "JA1XYZ", "25");
Assert.True(session.Verdict()?.IsDupe);
}
[Fact]
public void MovingBandClearsTheDupe()
{
RadioPosition 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()
{
RadioPosition 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()
{
RadioPosition 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)
{
RadioPosition session = Session();
session.Entry.Call = typed;
Assert.Equal(hertz, session.PendingQsy()?.Hertz);
}
[Theory]
[InlineData("JA1XYZ")]
[InlineData("12000")]
public void ACallOrAnOutOfBandNumberIsNotAQsy(string typed)
{
RadioPosition session = Session();
session.Entry.Call = typed;
Assert.Null(session.PendingQsy());
}
[Fact]
public void AContactDeletedFromTheLogGivesItsMultiplierBack()
{
RadioPosition session = Session();
Type(session, "JA1XYZ", "25");
Qso first = session.LogContact();
Type(session, "JA2XYZ", "25");
session.LogContact();
Assert.Equal(2, session.Log.Tally.TotalMultipliers);
session.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()
{
RadioPosition 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()
{
RadioPosition session = new(Shared(new FakeLogStore()), 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()
{
RadioPosition 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()
{
RadioPosition session = Session();
Type(session, "JA1XYZ", "25");
Qso logged = session.LogContact();
Assert.Equal(3, session.Log.Tally.Points);
QsoEdit edit = session.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()
{
RadioPosition session = Session();
Type(session, "JA1XYZ", "25");
Qso logged = session.LogContact();
QsoEdit edit = session.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()
{
RadioPosition session = Session();
Type(session, "JA1XYZ", "25");
Qso logged = session.LogContact();
QsoChange? change = null;
session.Session.Edited += (_, c) => change = c;
session.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()
{
RadioPosition session = Session();
Type(session, "JA1XYZ", "25");
Qso logged = session.LogContact();
Assert.Equal(3, session.Log.Tally.Points);
session.Session.Edit(logged.Id, QsoField.CountryPrefix, "DL");
Assert.Equal(0, session.Log.Tally.Points);
}
[Fact]
public void ADeleteReportsTheContactThatWent()
{
RadioPosition session = Session();
Type(session, "JA1XYZ", "25");
Qso logged = session.LogContact();
Qso? gone = null;
session.Session.Deleted += (_, q) => gone = q;
session.Session.Delete(logged.Id);
Assert.Equal("JA1XYZ", gone?.Call.Text);
}
[Fact]
public void StoredContactsAreReadBackWhenTheSessionStarts()
{
FakeLogStore store = new();
RadioPosition first = new(Shared(store));
Type(first, "JA1XYZ", "25");
first.LogContact();
RadioPosition second = new(new ContestSession(
store,
new CqWorldWide(ModeCategory.Cw),
first.Instance,
Me,
CountryFile.Parse(Countries)));
Assert.Single(second.Log.Qsos);
Assert.Equal(3, second.Log.Tally.Points);
}
private static RadioPosition WithHistory(Contest contest, string file)
{
FakeLogStore store = new();
ContestInstance instance = store.AddContest(new ContestInstance
{
ContestNumber = 0,
ContestName = contest.Name,
});
return new RadioPosition(new ContestSession(
store,
contest,
instance,
Me,
CountryFile.Parse(Countries),
CallHistory.Parse(file)));
}
[Fact]
public void TheCallHistoryFillsTheExchangeIn()
{
RadioPosition session = WithHistory(
new Sweepstakes(ModeCategory.Cw),
"!!Order!!,Call,Sect,CK\nW3LPL,MDC,56");
session.Entry.Call = "W3LPL";
Assert.True(session.FillFromHistory());
Assert.Equal("MDC", session.Entry.ValueOf(ExchangeSlot.Section));
Assert.Equal("56", session.Entry.ValueOf(ExchangeSlot.Check));
}
/// What the other station actually sends beats what was published, so a box
/// with something in it is left alone.
[Fact]
public void WhatIsAlreadyTypedIsNotOverwritten()
{
RadioPosition session = WithHistory(
new Sweepstakes(ModeCategory.Cw),
"!!Order!!,Call,Sect\nW3LPL,MDC");
session.Entry.Call = "W3LPL";
session.Entry.Set(ExchangeSlot.Section, "EPA");
session.FillFromHistory();
Assert.Equal("EPA", session.Entry.ValueOf(ExchangeSlot.Section));
}
[Fact]
public void ACallTheHistoryDoesNotHoldFillsNothing()
{
RadioPosition session = WithHistory(
new Sweepstakes(ModeCategory.Cw),
"!!Order!!,Call,Sect\nW3LPL,MDC");
session.Entry.Call = "JA1XYZ";
Assert.False(session.FillFromHistory());
Assert.Equal("", session.Entry.ValueOf(ExchangeSlot.Section));
}
/// CQ WW exchanges the CQ zone, so the CQ zone column is the one to use.
[Fact]
public void TheZoneComesFromTheColumnTheContestAsksFor()
{
RadioPosition session = WithHistory(
new CqWorldWide(ModeCategory.Cw),
"!!Order!!,Call,CqZone,ITUZone\nJA1XYZ,25,45");
session.Entry.Call = "JA1XYZ";
session.FillFromHistory();
Assert.Equal("25", session.Entry.ValueOf(ExchangeSlot.Zone));
}
/// A file for a section contest often carries the state instead.
[Fact]
public void TheStateFillsASectionWhenTheFileHasNoSection()
{
RadioPosition session = WithHistory(
new Sweepstakes(ModeCategory.Cw),
"!!Order!!,Call,Sect,State\nK1TTT,,CT");
session.Entry.Call = "K1TTT";
session.FillFromHistory();
Assert.Equal("CT", session.Entry.ValueOf(ExchangeSlot.Section));
}
[Fact]
public void TheNameAndCommentBoxesReachTheLog()
{
RadioPosition session = Session();
session.Entry.Call = "JA1XYZ";
session.Entry.Set(ExchangeSlot.ReceivedReport, "599");
session.Entry.Set(ExchangeSlot.Zone, "25");
session.OtherName = "Ken";
session.Comment = "up 2";
Qso logged = session.LogContact();
Assert.Equal("Ken", logged.Name);
Assert.Equal("up 2", logged.Comment);
Assert.Equal("", session.OtherName);
Assert.Equal("", session.Comment);
}
[Fact]
public void WipingClearsTheNameAndTheComment()
{
RadioPosition session = Session();
session.OtherName = "Ken";
session.Comment = "up 2";
session.Wipe();
Assert.Equal("", session.OtherName);
Assert.Equal("", session.Comment);
}
}