Files
Nonemm/tests/Nonemm.Session.Tests/NetworkedContactsTests.cs
T
erikbrockoandClaude Opus 5 0b92bfc6ee Fill in the rest of N1MM's network status window
Seven tabs now, in N1MM's order, and the messages behind them.

**Options** holds what there is something behind: Log QSOs at all Stations,
which decides whether the contacts the others log are kept here; the
message trace, moved off the Messages tab where N1MM does not keep it; and
Run1/Run2, which used to be guessed from the radio number and is now the
operator's choice, sent in every `STATUS` and every `XMIT`. The transmit
interlocks stay out: this program does not block a transmitter, and a
station that claimed it did would be waited on for nothing.

**Call Stack Targets.** A callsign heard at one station and handed to the
position that will work it — `STACKANOTHERCALL` with the call alone,
`STACKCALL` with the contact behind it. The target is picked from the tab
or from the station list, and what arrives goes on the call stack of the
radio the operator is on, sorted by whether it is a multiplier here.

**The master** is the station the entry follows for cluster spots and the
time. `MASTER` names it to everybody, so the column agrees everywhere.

**The clock.** `TIME` carries a station's clock and Force Time Sync sends
it. N1MM sets the clock from it when it is running as an administrator and
says how far out it is when it is not; this program always does the second,
because setting the clock needs root and a contest logger has no business
asking for it. Anything over two seconds is called out.

**Serial numbers.** Reset Serial Numbers at all Stations sends
`RESETQSONRS` and every station starts again at one. The contacts already
logged keep the numbers they were sent with.

**What the entry agrees on** is now checked rather than assumed:

| What | How |
|---|---|
| the contest | `CONTESTNAME` as soon as a connection opens, and again in every `CHECKSUM` |
| the country file | its issue date — the publisher's `=VER20260826` — rides in `STATUS` |
| the computer name | longer than N1MM's fifteen characters is named at startup; a second computer answering to this one's name is named when its beacon arrives |
| the station number | two stations on the same number are named when the second says so |

`CountryFile` reads its issue date, which nothing had asked it for before.

Left for later: the master relaying its cluster spots, and the shared
serial-number pool where stations claim numbers in advance.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdAYHcdRktqKry7nk414TU
2026-09-03 23:15:44 +00:00

280 lines
9.4 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.SentNumber);
session.SerialNumbersFrom = DateTime.UtcNow;
Assert.Equal(1, session.SentNumber);
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));
}
}