Let the walk and the completeness check read one rule
A box the walk between the entry boxes steps over cannot be typed in, so it must not hold the contact back either. `EntryFields.IsComplete` counted every box marked required, while `MoveFocus` stepped over the ones the contest does not ask this station for, so a contest with a box for some stations only could lock the operator out of his own log. The seven contests the scoring audit named do not do that any more — `bae40bb` gave each of them the boxes N1MM shows, and fifteen reproductions across all seven and both station types log cleanly — but the rule they broke was still there to break again. Completeness now takes the same applicability rule the cursor uses, and `OperatingPosition` answers it in one place for the entry window, the ESM window and logging alike. The audit is marked up for the three findings that are already fixed and for item 14, which turns out to be what N1MM does rather than a fault. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
using Nonemm.Contests;
|
||||
using Nonemm.Core;
|
||||
using Nonemm.Core.Country;
|
||||
using Nonemm.Storage;
|
||||
|
||||
namespace Nonemm.Session.Tests;
|
||||
|
||||
/// A box the walk between the entry boxes steps over cannot be typed in, so it
|
||||
/// must not be what holds a contact back either. CQ WW RTTY is the contest
|
||||
/// that skips a box today, and its state box is not required; this is the rule
|
||||
/// itself, so a contest that asks only some stations for a box cannot lock the
|
||||
/// operator out of his own log.
|
||||
public class SkippedFieldTests
|
||||
{
|
||||
private const string Countries = """
|
||||
United States: 05: 08: NA: 37.60: 91.87: 5.0: K:
|
||||
K,W,N,AA;
|
||||
Japan: 25: 45: AS: 36.40: -138.38: -9.0: JA:
|
||||
JA,JH,JR;
|
||||
Germany: 14: 28: EU: 51.00: -10.00: -1.0: DL:
|
||||
DL,DK;
|
||||
""";
|
||||
|
||||
private static readonly StationInfo Me = new()
|
||||
{
|
||||
Callsign = "DL1ABC",
|
||||
CountryPrefix = "DL",
|
||||
Continent = "EU",
|
||||
CqZone = 14,
|
||||
};
|
||||
|
||||
private static OperatingPosition Position()
|
||||
{
|
||||
FakeLogStore store = new();
|
||||
ContestInstance instance = store.AddContest(new ContestInstance
|
||||
{
|
||||
ContestNumber = 0,
|
||||
ContestName = "STATEONLY",
|
||||
});
|
||||
return new OperatingPosition(new ContestSession(
|
||||
store, new StateFromAmericansOnly(), instance, Me, CountryFile.Parse(Countries)));
|
||||
}
|
||||
|
||||
private static OperatingPosition Working(string call)
|
||||
{
|
||||
OperatingPosition position = Position();
|
||||
position.Entry.Call = call;
|
||||
position.Entry.Set(ExchangeSlot.ReceivedReport, "599");
|
||||
return position;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ABoxTheWalkStepsOverDoesNotHoldTheContactBack()
|
||||
{
|
||||
OperatingPosition position = Working("JA1XYZ");
|
||||
|
||||
Assert.True(position.IsEntryComplete);
|
||||
Assert.Equal("JA1XYZ", position.LogContact().Call.Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ABoxTheStationIsAskedForStillHoldsTheContactBack()
|
||||
{
|
||||
OperatingPosition position = Working("K1ABC");
|
||||
|
||||
Assert.False(position.IsEntryComplete);
|
||||
Assert.Throws<InvalidOperationException>(() => position.LogContact());
|
||||
|
||||
position.Entry.Set(ExchangeSlot.Section, "CT");
|
||||
|
||||
Assert.True(position.IsEntryComplete);
|
||||
}
|
||||
|
||||
/// The cursor and the completeness check read the same rule. The report
|
||||
/// boxes are filled in by the program, so with the state box stepped over
|
||||
/// there is nothing left to type and the cursor stays on the callsign.
|
||||
[Fact]
|
||||
public void TheWalkStepsOverTheSameBox()
|
||||
{
|
||||
OperatingPosition japan = Working("JA1XYZ");
|
||||
japan.Entry.FocusOn(0);
|
||||
japan.MoveFocus(forward: true);
|
||||
|
||||
Assert.Equal(0, japan.Entry.Focus);
|
||||
|
||||
OperatingPosition america = Working("K1ABC");
|
||||
america.Entry.FocusOn(0);
|
||||
america.MoveFocus(forward: true);
|
||||
|
||||
Assert.Equal(
|
||||
ExchangeSlot.Section,
|
||||
america.Entry.Exchange[america.Entry.Focus - 1].Slot);
|
||||
}
|
||||
|
||||
/// A contest that asks only American stations for a state, and marks the
|
||||
/// box required — which is what the entry window used to lock up on.
|
||||
private sealed class StateFromAmericansOnly : Contest
|
||||
{
|
||||
public string Name => "STATEONLY";
|
||||
|
||||
public string DisplayName => "State from Americans only";
|
||||
|
||||
public string CabrilloName => "STATE-ONLY";
|
||||
|
||||
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
|
||||
[
|
||||
new ExchangeField("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
|
||||
new ExchangeField("Rcv", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
|
||||
new ExchangeField("State", ExchangeSlot.Section, ExchangeFieldKind.Text),
|
||||
];
|
||||
|
||||
public bool SkipsField(ExchangeField field, CountryLookup? their) =>
|
||||
field.Slot == ExchangeSlot.Section && their?.Entity.PrimaryPrefix != "K";
|
||||
|
||||
public IReadOnlyList<string> MultiplierNames => ["States"];
|
||||
|
||||
public DupeScope DupeScope => DupeScope.PerBand;
|
||||
|
||||
public bool HasSerialNumbers => false;
|
||||
|
||||
public IReadOnlyList<ModeCategory> Modes => [];
|
||||
|
||||
public string SentExchangeFor(StationInfo me) => me.State;
|
||||
|
||||
public int PointsFor(QsoContext qso) => 1;
|
||||
|
||||
public IReadOnlyList<Multiplier> MultipliersFor(QsoContext qso) => [];
|
||||
|
||||
public int TotalScore(ScoreTally tally, ContestEntry entry) => tally.Points;
|
||||
|
||||
public CabrilloExchange CabrilloExchange(Qso qso, StationInfo me, ContestEntry entry) =>
|
||||
new([new CabrilloField(me.Callsign, 13)], [new CabrilloField(qso.Call.Text, 13)]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user