Take a contact from another station without reopening the contest
AppSession.TakeFromNetwork wrote the contact to the store and then called OpenContest to see it, which built a new ContestSession, read the whole log back and built new entry positions. Anything the operator was typing at either radio went with them, and the entry boxes were rebuilt under the other operator's hands. It also ran on the socket thread, so it was changing the log while the windows were reading it. ContestSession now applies the three messages itself: AddFromNetwork, ReplaceFromNetwork and DeleteFromNetwork write the store, change the log in place and raise Changed. None of them raises Logged, Edited or Deleted, so nothing goes back out to the other stations. Points and multipliers are still worked out here from the rules rather than trusted. A contact that arrives goes in where its timestamp says it belongs, through the new ContestLog.Insert, and the log is scored again: of two stations that worked the same multiplier, the one that worked it first keeps it, however late the message turns up. Add would have appended it at the end and given the multiplier to the wrong contact. The socket thread now hands the update to the thread the windows run on, which is where every other change to the log is made. The matching rule moved with the code, so ContestSession answers what a message from N1MM refers to: the contact id, then the call and time it had before the edit. Still not tested against a second station. Two of these cannot run on one host to try it, because they share one UDP port. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
This commit is contained in:
202
tests/Nonemm.Session.Tests/NetworkedContactsTests.cs
Normal file
202
tests/Nonemm.Session.Tests/NetworkedContactsTests.cs
Normal file
@@ -0,0 +1,202 @@
|
||||
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));
|
||||
}
|
||||
|
||||
[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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user