Files
Nonemm/tests/Nonemm.Session.Tests/OperatingPositionTests.cs
T
erikbrockoandClaude Opus 5 d5788da240 Wipe on Alt+W as well as put the boxes back
Alt+W only put the boxes back, so wiping needed Ctrl+W or the button.
It now goes both ways, the way N1MM's Wipe button does: with something in
the boxes it clears them, and pressed again on empty boxes it puts back
what it cleared.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015NZkFxHde1iifAQemFTHxf
2026-09-10 11:30:07 +00:00

731 lines
23 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 OperatingPositionTests
{
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 OperatingPosition 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(OperatingPosition session, string call, string zone)
{
session.Entry.Call = call;
session.Entry.Set(ExchangeSlot.ReceivedReport, "599");
session.Entry.Set(ExchangeSlot.Zone, zone);
}
/// N1MM shows what this station sends beside what it copies, keeps both
/// filled in and steps over them.
[Fact]
public void TheSentBoxesAreFilledInAndSteppedOver()
{
OperatingPosition session = Session(new CqWpx(ModeCategory.Cw));
Assert.Equal("599", session.Entry.ValueOf(ExchangeSlot.SentReport));
Assert.Equal("1", session.Entry.ValueOf(ExchangeSlot.SentNumber));
// the boxes are the call, the two reports, the sent serial and the
// serial being copied
session.Entry.Advance();
Assert.Equal(4, session.Entry.Focus);
}
[Fact]
public void TheSentSerialGoesUpWhenAContactIsLogged()
{
OperatingPosition session = Session(new CqWpx(ModeCategory.Cw));
session.Entry.Call = "JA1XYZ";
session.Entry.Set(ExchangeSlot.ReceivedReport, "599");
session.Entry.Set(ExchangeSlot.SerialNumber, "12");
Assert.Equal(1, session.LogContact().SentNumber);
Assert.Equal("2", session.Entry.ValueOf(ExchangeSlot.SentNumber));
}
/// The BARTG contest sends the time of the exchange, and logs the time it
/// sent rather than the time the contact was logged.
[Fact]
public void TheTimeTheExchangeWentOutIsLoggedForBartg()
{
OperatingPosition session = Session(new BartgSprint());
session.Entry.Call = "JA1XYZ";
session.Entry.Set(ExchangeSlot.ReceivedReport, "599");
session.Entry.Set(ExchangeSlot.SerialNumber, "12");
session.Entry.Set(ExchangeSlot.Exchange1, "1646");
session.StampSentTime();
string stamped = session.SentTime;
Qso logged = session.LogContact();
Assert.Equal(4, stamped.Length);
Assert.Equal(stamped, logged.MiscText);
Assert.Equal("", session.SentTime);
}
/// A contest that does not exchange a time keeps the column empty.
[Fact]
public void NothingIsStampedForAContestThatSendsNoTime()
{
OperatingPosition session = Session();
Type(session, "JA1XYZ", "25");
session.StampSentTime();
Assert.Equal("", session.SentTime);
Assert.Equal("", session.LogContact().MiscText);
}
[Fact]
public void LoggingAContactPutsItInTheLogAndClearsTheEntry()
{
OperatingPosition 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()
{
OperatingPosition 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()
{
OperatingPosition session = Session();
session.Entry.Call = "JA1XYZ";
Assert.Throws<InvalidOperationException>(() => session.LogContact());
}
[Fact]
public void TheSameStationOnTheSameBandReadsAsADupe()
{
OperatingPosition session = Session();
Type(session, "JA1XYZ", "25");
session.LogContact();
Type(session, "JA1XYZ", "25");
Assert.True(session.Verdict()?.IsDupe);
}
[Fact]
public void MovingBandClearsTheDupe()
{
OperatingPosition 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()
{
OperatingPosition 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 SpaceStepsOverTheReportAndComesBackToTheCall()
{
// CQ WW's boxes are the call, the two reports and the zone
OperatingPosition session = Session();
Assert.Equal(0, session.Entry.Focus);
session.Entry.Advance();
Assert.Equal(3, session.Entry.Focus);
session.Entry.Advance();
Assert.Equal(0, session.Entry.Focus);
}
[Fact]
public void GoingBackStepsOverTheReportToo()
{
OperatingPosition session = Session();
session.Entry.FocusOn(3);
session.Entry.Retreat();
Assert.Equal(0, session.Entry.Focus);
}
[Fact]
public void TheReportBoxesAreFilledInWithWhatWouldBeSentAnyway()
{
OperatingPosition session = Session();
Assert.True(session.FillReports());
Assert.Equal("599", session.Entry.ValueOf(ExchangeSlot.ReceivedReport));
Assert.False(session.FillReports());
}
[Fact]
public void AReportTheOperatorTypedIsLeftAlone()
{
OperatingPosition session = Session();
session.Entry.Set(ExchangeSlot.ReceivedReport, "559");
Assert.False(session.FillReports());
Assert.Equal("559", session.Entry.ValueOf(ExchangeSlot.ReceivedReport));
}
[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)
{
OperatingPosition session = Session();
session.Entry.Call = typed;
Assert.Equal(hertz, session.PendingQsy()?.Hertz);
}
[Theory]
[InlineData("JA1XYZ")]
[InlineData("12000")]
public void ACallOrAnOutOfBandNumberIsNotAQsy(string typed)
{
OperatingPosition session = Session();
session.Entry.Call = typed;
Assert.Null(session.PendingQsy());
}
[Fact]
public void AContactDeletedFromTheLogGivesItsMultiplierBack()
{
OperatingPosition 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()
{
OperatingPosition 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()
{
OperatingPosition 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()
{
OperatingPosition 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()
{
OperatingPosition 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()
{
OperatingPosition 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()
{
OperatingPosition 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()
{
OperatingPosition 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()
{
OperatingPosition 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();
OperatingPosition first = new(Shared(store));
Type(first, "JA1XYZ", "25");
first.LogContact();
OperatingPosition 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 OperatingPosition WithHistory(Contest contest, string file)
{
FakeLogStore store = new();
ContestInstance instance = store.AddContest(new ContestInstance
{
ContestNumber = 0,
ContestName = contest.Name,
});
return new OperatingPosition(new ContestSession(
store,
contest,
instance,
Me,
CountryFile.Parse(Countries),
CallHistory.Parse(file)));
}
[Fact]
public void TheCallHistoryFillsTheExchangeIn()
{
OperatingPosition session = WithHistory(
new Sweepstakes(ModeCategory.Cw),
"!!Order!!,Call,Sect,CK\nW3LPL,MDC,56");
session.Entry.Call = "W3LPL";
Assert.True(session.FillFromHistory());
Assert.Equal("56MDC", session.Entry.ValueOf(ExchangeSlot.Exchange1));
}
/// What the other station actually sends beats what was published, so a box
/// with something in it is left alone.
[Fact]
public void WhatIsAlreadyTypedIsNotOverwritten()
{
OperatingPosition session = WithHistory(
new Sweepstakes(ModeCategory.Cw),
"!!Order!!,Call,Sect\nW3LPL,MDC");
session.Entry.Call = "W3LPL";
session.Entry.Set(ExchangeSlot.Exchange1, "72A 74EPA");
session.FillFromHistory();
Assert.Equal("72A 74EPA", session.Entry.ValueOf(ExchangeSlot.Exchange1));
}
[Fact]
public void ACallTheHistoryDoesNotHoldFillsNothing()
{
OperatingPosition 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.Exchange1));
}
/// CQ WW exchanges the CQ zone, so the CQ zone column is the one to use.
[Fact]
public void TheZoneComesFromTheColumnTheContestAsksFor()
{
OperatingPosition 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()
{
OperatingPosition session = WithHistory(
new Sweepstakes(ModeCategory.Cw),
"!!Order!!,Call,Sect,State\nK1TTT,,CT");
session.Entry.Call = "K1TTT";
session.FillFromHistory();
Assert.Equal("00CT", session.Entry.ValueOf(ExchangeSlot.Exchange1));
}
[Fact]
public void TheNameAndCommentBoxesReachTheLog()
{
OperatingPosition 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()
{
OperatingPosition session = Session();
session.OtherName = "Ken";
session.Comment = "up 2";
session.Wipe();
Assert.Equal("", session.OtherName);
Assert.Equal("", session.Comment);
}
[Fact]
public void CqWwRttyStepsOverTheStateBoxForAStationOutsideTheUsAndCanada()
{
OperatingPosition session = Session(new CqWorldWide(ModeCategory.Digital));
session.Entry.Call = "JA1XYZ";
// the boxes are the call, the two reports, the zone and the state
session.MoveFocus(forward: true);
Assert.Equal(3, session.Entry.Focus);
session.MoveFocus(forward: true);
Assert.Equal(0, session.Entry.Focus);
}
[Fact]
public void CqWwRttyStopsOnTheStateBoxForAUsStation()
{
OperatingPosition session = Session(new CqWorldWide(ModeCategory.Digital));
session.Entry.Call = "K1ABC";
session.MoveFocus(forward: true);
Assert.Equal(3, session.Entry.Focus);
session.MoveFocus(forward: true);
Assert.Equal(4, session.Entry.Focus);
}
[Fact]
public void QuickEditLoadsTheLastContactAndSavesTheChangeBack()
{
OperatingPosition session = Session();
Type(session, "JA1XYZ", "25");
Qso logged = session.LogContact();
Type(session, "DL9ZZZ", "14");
Assert.True(session.QuickEdit(forward: false));
Assert.Equal(logged.Id, session.Editing?.Id);
Assert.Equal("JA1XYZ", session.Entry.Call);
Assert.Equal("25", session.Entry.ValueOf(ExchangeSlot.Zone));
session.Entry.Set(ExchangeSlot.Zone, "24");
Qso? saved = session.SaveQuickEdit();
Assert.Equal(24, saved?.Zone);
Assert.Equal(24, session.Log.Qsos[0].Zone);
Assert.Null(session.Editing);
}
[Fact]
public void LeavingQuickEditPutsBackWhatWasBeingTyped()
{
OperatingPosition session = Session();
Type(session, "JA1XYZ", "25");
session.LogContact();
Type(session, "DL9ZZZ", "14");
session.QuickEdit(forward: false);
session.LeaveQuickEdit();
Assert.Null(session.Editing);
Assert.Equal("DL9ZZZ", session.Entry.Call);
Assert.Equal("14", session.Entry.ValueOf(ExchangeSlot.Zone));
}
[Fact]
public void QuickEditForwardPastTheNewestContactLeavesQuickEdit()
{
OperatingPosition session = Session();
Type(session, "JA1XYZ", "25");
session.LogContact();
Type(session, "DL9ZZZ", "14");
session.QuickEdit(forward: false);
Assert.True(session.QuickEdit(forward: true));
Assert.Null(session.Editing);
Assert.Equal("DL9ZZZ", session.Entry.Call);
}
[Fact]
public void QuickEditStepsBackThroughTheLog()
{
OperatingPosition session = Session();
Type(session, "JA1XYZ", "25");
session.LogContact();
Type(session, "DL9ZZZ", "14");
session.LogContact();
session.QuickEdit(forward: false);
Assert.Equal("DL9ZZZ", session.Entry.Call);
session.QuickEdit(forward: false);
Assert.Equal("JA1XYZ", session.Entry.Call);
Assert.False(session.QuickEdit(forward: false));
}
/// The entry boxes and the log window take the same values. A zone that
/// does not exist is refused in both, and the box holding it is named so
/// the entry window can put the cursor back in it.
[Fact]
public void AZoneThatDoesNotExistCannotBeLogged()
{
OperatingPosition session = Session();
Type(session, "JA1XYZ", "41");
EntryProblem? problem = session.EntryProblem();
Assert.Equal("CQ zones run from 1 to 40", problem!.Message);
Assert.Equal("41", session.Entry[problem.Field]);
Assert.Throws<InvalidOperationException>(session.LogContact);
}
[Fact]
public void ArbitraryTextInTheReportBoxCannotBeLogged()
{
OperatingPosition session = Session();
Type(session, "JA1XYZ", "25");
session.Entry.Set(ExchangeSlot.ReceivedReport, "good copy");
Assert.Contains("signal report", session.EntryProblem()!.Message);
}
[Fact]
public void ACallThatIsNotShapedLikeOneCannotBeLogged()
{
OperatingPosition session = Session();
Type(session, "12345", "25");
Assert.Equal(0, session.EntryProblem()!.Field);
}
[Fact]
public void AnExchangeTheContestTakesGivesNoProblem()
{
OperatingPosition session = Session();
Type(session, "ja1xyz", "25");
Assert.Null(session.EntryProblem());
}
/// Nothing is typed yet when the operator is between contacts, and that is
/// not something to complain about.
[Fact]
public void EmptyBoxesGiveNoProblem() =>
Assert.Null(Session().EntryProblem());
/// N1MM's Alt+W goes both ways: it wipes the boxes, and pressed again on
/// empty boxes it puts back what it wiped.
[Fact]
public void AltWWipesAndThenPutsTheBoxesBack()
{
OperatingPosition session = Session();
Type(session, "JA1XYZ", "25");
session.WipeOrRestore();
Assert.Equal("", session.Entry.Call);
session.WipeOrRestore();
Assert.Equal("JA1XYZ", session.Entry.Call);
Assert.Equal("25", session.Entry.ValueOf(ExchangeSlot.Zone));
}
/// A wipe keeps what was in the boxes and Alt+W puts it back.
[Fact]
public void WhatAWipeClearedComesBack()
{
OperatingPosition session = Session();
Type(session, "JA1XYZ", "25");
session.Comment = "loud";
session.Wipe();
Assert.Equal("", session.Entry.Call);
Assert.True(session.RestoreWiped());
Assert.Equal("JA1XYZ", session.Entry.Call);
Assert.Equal("25", session.Entry.ValueOf(ExchangeSlot.Zone));
Assert.Equal("loud", session.Comment);
}
[Fact]
public void ThereIsNothingToPutBackBeforeAWipe() =>
Assert.False(Session().RestoreWiped());
/// Wiping empty boxes would otherwise throw away what the wipe before it
/// kept.
[Fact]
public void ASecondWipeKeepsTheFirstOne()
{
OperatingPosition session = Session();
Type(session, "JA1XYZ", "25");
session.Wipe();
session.Wipe();
Assert.True(session.RestoreWiped());
Assert.Equal("JA1XYZ", session.Entry.Call);
}
/// N1MM puts the boxes back only when they are empty, so typing is never
/// overwritten by a restore.
[Fact]
public void NothingIsPutBackOverSomethingTyped()
{
OperatingPosition session = Session();
Type(session, "JA1XYZ", "25");
session.Wipe();
session.Entry.Call = "DL2XYZ";
Assert.False(session.RestoreWiped());
Assert.Equal("DL2XYZ", session.Entry.Call);
}
/// A call put on the stack has not gone anywhere, so there is nothing to
/// put back and Alt+W would only make a second copy of it.
[Fact]
public void AStackedCallIsNotWaitingToBePutBack()
{
OperatingPosition session = Session();
session.IsRunning = true;
Type(session, "JA1XYZ", "25");
Assert.True(session.StackAnother());
Assert.False(session.RestoreWiped());
}
}