Each station worked out its next number as the highest in its log plus one, so two operators typing at the same moment both sent the same number and one of the two contacts was wrong in the sponsor's log. SerialNumberPool holds the highest number taken per band, which is N1MM's QSONumbers array. A number is taken at the first character of the callsign, as N1MM takes it, and ReserveNr tells the other stations before the contact is worked. A number at or below one this station has used is answered with the whole pool, N1MM's QSONRS, and the station that is behind counts on from there. The pool also goes out when a contest is opened and when the numbers are started again. Contest.NumbersByBand says whether the numbers run per band or as one sequence for the station. CQ WPX runs one per band for a multi-two and a multi-multi; every other contest here runs one sequence. The band index in the message is N1MM's, so the array lines up with theirs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015NZkFxHde1iifAQemFTHxf
280 lines
9.5 KiB
C#
280 lines
9.5 KiB
C#
using Nonemm.Contests;
|
|
using Nonemm.Contests.Rules;
|
|
using Nonemm.Core;
|
|
using Nonemm.Core.Country;
|
|
using Nonemm.Storage;
|
|
|
|
namespace Nonemm.Session.Tests;
|
|
|
|
/// What a contact from another station of a multi-operator entry does to this
|
|
/// log.
|
|
public class NetworkedContactsTests
|
|
{
|
|
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 readonly DateTime Start = new(2026, 1, 1, 12, 0, 0, DateTimeKind.Utc);
|
|
|
|
private static (ContestSession Session, FakeLogStore Store) Station()
|
|
{
|
|
FakeLogStore store = new();
|
|
ContestInstance instance = store.AddContest(new ContestInstance
|
|
{
|
|
ContestNumber = 0,
|
|
ContestName = "CQWW",
|
|
});
|
|
return (
|
|
new ContestSession(
|
|
store,
|
|
new CqWorldWide(ModeCategory.Cw),
|
|
instance,
|
|
Me,
|
|
CountryFile.Parse(Countries)),
|
|
store);
|
|
}
|
|
|
|
private static Qso Arriving(string call, int minute, int zone = 25) => new()
|
|
{
|
|
Id = $"{call}-{minute}",
|
|
TimestampUtc = Start.AddMinutes(minute),
|
|
Call = Callsign.Parse(call),
|
|
Frequency = Frequency.FromKilohertz(14_030),
|
|
Mode = Modes.Cw,
|
|
ContestName = "CQWW",
|
|
SentReport = "599",
|
|
ReceivedReport = "599",
|
|
Zone = zone,
|
|
// what the sending station worked out, which is not trusted
|
|
Points = 99,
|
|
IsMultiplier1 = true,
|
|
};
|
|
|
|
/// A contact worked at this station, which `Add` scores from the rules like
|
|
/// any other.
|
|
private static Qso WorkHere(ContestSession session, string call, int minute, int zone = 25) =>
|
|
session.Add(Arriving(call, minute, zone) with
|
|
{
|
|
Id = Qso.NewId(),
|
|
ContestNumber = session.Instance.ContestNumber,
|
|
});
|
|
|
|
[Fact]
|
|
public void AContactFromAnotherStationIsLoggedAndScoredHere()
|
|
{
|
|
(ContestSession session, _) = Station();
|
|
|
|
Assert.True(session.AddFromNetwork(Arriving("JA1XYZ", 1)));
|
|
|
|
Qso stored = Assert.Single(session.Log.Qsos);
|
|
Assert.Equal("JA1XYZ", stored.Call.Text);
|
|
Assert.Equal(3, stored.Points);
|
|
Assert.False(stored.IsOriginal);
|
|
Assert.Equal(session.Instance.ContestNumber, stored.ContestNumber);
|
|
}
|
|
|
|
[Fact]
|
|
public void ItDoesNotGoBackOutToTheOtherStations()
|
|
{
|
|
(ContestSession session, _) = Station();
|
|
int sent = 0;
|
|
session.Logged += (_, _) => sent++;
|
|
|
|
session.AddFromNetwork(Arriving("JA1XYZ", 1));
|
|
|
|
Assert.Equal(0, sent);
|
|
}
|
|
|
|
[Fact]
|
|
public void TheSameContactArrivingTwiceIsTakenOnce()
|
|
{
|
|
(ContestSession session, _) = Station();
|
|
session.AddFromNetwork(Arriving("JA1XYZ", 1));
|
|
|
|
Assert.False(session.AddFromNetwork(Arriving("JA1XYZ", 1)));
|
|
Assert.Single(session.Log.Qsos);
|
|
}
|
|
|
|
/// The station that worked it first keeps the multiplier, however late the
|
|
/// message about it turns up.
|
|
[Fact]
|
|
public void AContactThatArrivesLateStillTakesTheMultiplierItWorkedFirst()
|
|
{
|
|
(ContestSession session, _) = Station();
|
|
WorkHere(session, "JA9ZZZ", minute: 5);
|
|
|
|
session.AddFromNetwork(Arriving("JA1XYZ", minute: 1));
|
|
|
|
Assert.True(session.Log.Qsos[0].IsMultiplier1);
|
|
Assert.False(session.Log.Qsos[1].IsMultiplier1);
|
|
Assert.Equal("JA1XYZ", session.Log.Qsos[0].Call.Text);
|
|
}
|
|
|
|
[Fact]
|
|
public void WhatIsBeingTypedIsLeftAlone()
|
|
{
|
|
(ContestSession session, _) = Station();
|
|
OperatingPosition position = new(session);
|
|
position.Entry.Call = "G4AB";
|
|
position.Entry.Set(ExchangeSlot.Zone, "14");
|
|
|
|
session.AddFromNetwork(Arriving("JA1XYZ", 1));
|
|
|
|
Assert.Equal("G4AB", position.Entry.Call);
|
|
Assert.Equal("14", position.Entry.ValueOf(ExchangeSlot.Zone));
|
|
}
|
|
|
|
/// The stored points and multiplier flags go stale when a contact from
|
|
/// another station arrives after the one that took the multiplier. The
|
|
/// rescore is what writes them back, because N1MM reads those columns
|
|
/// rather than working them out.
|
|
[Fact]
|
|
public void ARescoreWritesTheScoringBackToTheStore()
|
|
{
|
|
(ContestSession session, FakeLogStore store) = Station();
|
|
Qso mine = WorkHere(session, "JA9ZZZ", minute: 5);
|
|
session.AddFromNetwork(Arriving("JA1XYZ", minute: 1));
|
|
Assert.True(store.Qsos(session.Instance.ContestNumber).Single(q => q.Id == mine.Id).IsMultiplier1);
|
|
|
|
Assert.Equal(2, session.Rescore(DateTime.MinValue));
|
|
|
|
Qso stored = store.Qsos(session.Instance.ContestNumber).Single(q => q.Id == mine.Id);
|
|
Assert.False(stored.IsMultiplier1);
|
|
Assert.Equal(session.Log.Qsos.Single(q => q.Id == mine.Id).Points, stored.Points);
|
|
}
|
|
|
|
/// A rescore of the last hour leaves the rows before it alone.
|
|
[Fact]
|
|
public void ARescoreOnlyWritesTheContactsInsideThePeriod()
|
|
{
|
|
(ContestSession session, _) = Station();
|
|
WorkHere(session, "JA9ZZZ", minute: 5);
|
|
WorkHere(session, "JA8YYY", minute: 90);
|
|
|
|
Assert.Equal(1, session.Rescore(Start.AddMinutes(60)));
|
|
}
|
|
|
|
/// A resync drops the block it is about to be sent again, so the contacts
|
|
/// are replaced rather than added to.
|
|
[Fact]
|
|
public void ABlockOfOneStationsContactsCanBeCleared()
|
|
{
|
|
(ContestSession session, FakeLogStore store) = Station();
|
|
session.AddFromNetwork(Arriving("JA1XYZ", minute: 1) with { StationName = "RUN-PC" });
|
|
session.AddFromNetwork(Arriving("JA2XYZ", minute: 90) with { StationName = "RUN-PC" });
|
|
session.AddFromNetwork(Arriving("JA3XYZ", minute: 2) with { StationName = "MULT-PC" });
|
|
|
|
Assert.Equal(1, session.ClearFromNetwork("run-pc", Start, TimeSpan.FromHours(1)));
|
|
|
|
Assert.Equal(["JA2XYZ", "JA3XYZ"], session.Log.Qsos.Select(q => q.Call.Text).Order());
|
|
Assert.Equal(2, store.Qsos(session.Instance.ContestNumber).Count);
|
|
}
|
|
|
|
/// Reset Serial Numbers at all Stations starts the numbers again at one.
|
|
/// The contacts already logged keep the numbers they were sent with.
|
|
[Fact]
|
|
public void SerialNumbersCanBeStartedAgainAtOne()
|
|
{
|
|
(ContestSession session, _) = Station();
|
|
session.Add(Arriving("JA1XYZ", 1) with { Id = Qso.NewId(), SentNumber = 1 });
|
|
session.Add(Arriving("JA2XYZ", 2) with { Id = Qso.NewId(), SentNumber = 2 });
|
|
Assert.Equal(3, session.NextNumberOn(null));
|
|
|
|
session.SerialNumbersFrom = DateTime.UtcNow;
|
|
|
|
Assert.Equal(1, session.NextNumberOn(null));
|
|
Assert.Equal([1, 2], session.Log.Qsos.Select(q => q.SentNumber));
|
|
}
|
|
|
|
/// Every contact logged here carries the computer that made it, which is
|
|
/// what a resync asks for and what N1MM stamps into `NetBiosName`.
|
|
[Fact]
|
|
public void AContactLoggedHereCarriesThisComputersName()
|
|
{
|
|
(ContestSession session, _) = Station();
|
|
session.StationName = "RUN-PC";
|
|
session.StationNumber = 3;
|
|
|
|
Qso stored = WorkHere(session, "JA1XYZ", minute: 1);
|
|
|
|
Assert.Equal("RUN-PC", stored.StationName);
|
|
Assert.Equal(3, stored.NetworkedComputerNumber);
|
|
}
|
|
|
|
[Fact]
|
|
public void AnEditFromAnotherStationIsAppliedById()
|
|
{
|
|
(ContestSession session, _) = Station();
|
|
session.AddFromNetwork(Arriving("JA1XYZ", 1));
|
|
|
|
Assert.True(session.ReplaceFromNetwork(
|
|
Arriving("JA1XYZ", 1) with { Zone = 24 },
|
|
"JA1XYZ",
|
|
Start.AddMinutes(1)));
|
|
|
|
Assert.Equal(24, Assert.Single(session.Log.Qsos).Zone);
|
|
}
|
|
|
|
/// N1MM does not know our ids, so a message from N1MM is matched on the
|
|
/// call and time the contact had before the edit.
|
|
[Fact]
|
|
public void AnEditWithNoIdIsMatchedOnTheOldCallAndTime()
|
|
{
|
|
(ContestSession session, _) = Station();
|
|
Qso worked = WorkHere(session, "JA1XYZ", minute: 1);
|
|
|
|
Assert.True(session.ReplaceFromNetwork(
|
|
Arriving("JA1XYY", 1) with { Id = "" },
|
|
"JA1XYZ",
|
|
worked.TimestampUtc));
|
|
|
|
Qso changed = Assert.Single(session.Log.Qsos);
|
|
Assert.Equal("JA1XYY", changed.Call.Text);
|
|
Assert.Equal(worked.Id, changed.Id);
|
|
}
|
|
|
|
[Fact]
|
|
public void AnEditToAContactWeDoNotHoldIsRefused()
|
|
{
|
|
(ContestSession session, _) = Station();
|
|
|
|
Assert.False(session.ReplaceFromNetwork(Arriving("JA1XYZ", 1), "JA1XYZ", Start));
|
|
Assert.Empty(session.Log.Qsos);
|
|
}
|
|
|
|
[Fact]
|
|
public void ADeleteFromAnotherStationRemovesTheContactAndRescores()
|
|
{
|
|
(ContestSession session, FakeLogStore store) = Station();
|
|
session.AddFromNetwork(Arriving("JA1XYZ", 1));
|
|
WorkHere(session, "JA9ZZZ", minute: 5);
|
|
|
|
Assert.True(session.DeleteFromNetwork("JA1XYZ-1", "JA1XYZ", Start.AddMinutes(1)));
|
|
|
|
Qso left = Assert.Single(session.Log.Qsos);
|
|
Assert.Equal("JA9ZZZ", left.Call.Text);
|
|
Assert.True(left.IsMultiplier1);
|
|
Assert.Single(store.Qsos(session.Instance.ContestNumber));
|
|
}
|
|
|
|
[Fact]
|
|
public void ADeleteOfAContactWeDoNotHoldIsRefused()
|
|
{
|
|
(ContestSession session, _) = Station();
|
|
|
|
Assert.False(session.DeleteFromNetwork("nothing", "JA1XYZ", Start));
|
|
}
|
|
}
|