Keep QTC traffic out of the serial numbers

A QTC row holds the serial of the contact it reports, which the station
being reported sent, not us. The serial pool was filled from every row in
the log, so reporting a station that sent us 1800 pushed our own next serial
to 1801, and reopening the log read it back from the rows again.

The pool now takes the sent number of contacts alone, when a contact is
logged, when one arrives from another station of the entry, and when the
numbers are seeded from the log at startup. N1MM leaves both QTC kinds out
of the same query, in QSONrsByBand.

The test opens a real log file, sends a series, closes the store and opens
it again, so the series numbering and the contacts already reported are
checked across a restart as well.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
This commit is contained in:
2026-09-10 19:30:32 +00:00
co-authored by Claude Opus 5
parent 3e326053a0
commit 265f90ccd7
2 changed files with 154 additions and 3 deletions
+16 -3
View File
@@ -111,7 +111,7 @@ public sealed class ContestSession
{
Log.Replace(stored);
}
Numbers.Record(stored.SentNumber, stored.Band);
RecordNumber(stored);
Logged?.Invoke(this, stored);
Changed?.Invoke(this, EventArgs.Empty);
return stored;
@@ -203,7 +203,7 @@ public sealed class ContestSession
}
Qso arrived = store.Add(FromNetwork(qso));
Log.Insert(arrived);
Numbers.Record(arrived.SentNumber, arrived.Band);
RecordNumber(arrived);
Changed?.Invoke(this, EventArgs.Empty);
return true;
}
@@ -274,6 +274,19 @@ public sealed class ContestSession
? Log.Qsos.Where(q =>
Contest.SessionOf(q.TimestampUtc) == Contest.SessionOf(DateTime.UtcNow))
: Log.Qsos;
Numbers.SeedFrom(counted);
Numbers.SeedFrom(counted.Where(Contest.IsContact));
}
/// The sent number of a contact, and nothing else. A WAE QTC row carries
/// the serial of the contact it reports in the same column, and that
/// number was sent by the station being reported rather than by us, so
/// taking it would push our own numbers up. N1MM leaves the QTC rows out
/// of the same query, in `QSONrsByBand`.
private void RecordNumber(Qso qso)
{
if (Contest.IsContact(qso))
{
Numbers.Record(qso.SentNumber, qso.Band);
}
}
}
@@ -0,0 +1,138 @@
using Microsoft.Data.Sqlite;
using Nonemm.Contests;
using Nonemm.Contests.Rules;
using Nonemm.Core;
using Nonemm.Core.Country;
using Nonemm.Storage;
namespace Nonemm.Session.Tests;
/// QTC traffic through a real log file and back: what a station has already
/// had, which contacts have already been reported and how the series are
/// numbered all come from the log rows, so they have to survive the program
/// being closed and opened again.
public class QtcRestartTests : IDisposable
{
private const string Countries = """
Germany: 14: 28: EU: 51.00: -10.00: -1.0: DL:
DL,DK,DJ;
United States: 05: 08: NA: 37.53: 91.67: 5.0: K:
K,W,N,AA,NE;
""";
private static readonly StationInfo UnitedStates = new()
{
Callsign = "NE1C",
CqZone = 5,
ItuZone = 8,
Continent = "NA",
CountryPrefix = "K",
};
private readonly string path = Path.Combine(Path.GetTempPath(), $"nonemm-{Guid.NewGuid():N}.s3db");
public void Dispose()
{
SqliteConnection.ClearAllPools();
File.Delete(path);
GC.SuppressFinalize(this);
}
/// Opens the log the way the program does when it starts: the store, then
/// the contest, then the position.
private OperatingPosition Open()
{
SqliteLogStore store = SqliteLogStore.Open(path);
ContestInstance instance = store.Contest(1) ?? store.AddContest(new ContestInstance
{
ContestNumber = 0,
ContestName = "WAECW",
ModeCategory = "CW",
});
return new OperatingPosition(new ContestSession(
store,
new Wae(ModeCategory.Cw),
instance,
UnitedStates,
CountryFile.Parse(Countries)));
}
private static Qso Contact(
OperatingPosition position,
string call,
int receivedNumber,
int minute) =>
position.Session.Add(new Qso
{
Id = Qso.NewId(),
TimestampUtc = new DateTime(2026, 11, 14, 13, minute, 0, DateTimeKind.Utc),
Call = Callsign.Parse(call),
Frequency = Frequency.FromKilohertz(14_025),
Mode = Modes.Cw,
ContestName = "WAECW",
ContestNumber = position.Instance.ContestNumber,
SentReport = "599",
ReceivedReport = "599",
SentNumber = position.Session.ReserveNumberOn(position.Band),
ReceivedNumber = receivedNumber,
Exchange1 = WaeQtc.Contact,
});
private static void Send(OperatingPosition position, IReadOnlyList<WaeQtc> lines)
{
for (int line = 0; line < lines.Count; line++)
{
position.Session.Add(lines[line].ApplyTo(new Qso
{
Id = Qso.NewId(),
TimestampUtc = new DateTime(2026, 11, 14, 14, 0, line, DateTimeKind.Utc),
Call = lines[line].Station,
Frequency = Frequency.FromKilohertz(14_025),
Mode = Modes.Cw,
ContestName = "WAECW",
ContestNumber = position.Instance.ContestNumber,
}));
}
}
[Fact]
public void TheSeriesCarriesOnAfterTheProgramIsOpenedAgain()
{
OperatingPosition first = Open();
QtcTraffic traffic = new(first);
Contact(first, "DL1AAA", receivedNumber: 16, minute: 40);
Contact(first, "DL2BBB", receivedNumber: 40, minute: 41);
Contact(first, "DL3CCC", receivedNumber: 7, minute: 42);
Send(first, traffic.ToSend(Callsign.Parse("DL9XYZ"), mostPerSeries: 2));
Assert.Equal(2, traffic.NextSeries());
OperatingPosition second = Open();
QtcTraffic reopened = new(second);
// the series number carries on rather than starting at one again
Assert.Equal(2, reopened.NextSeries());
// and the two contacts already reported are not offered a second time
Assert.Equal(
["DL3CCC"],
reopened.ToSend(Callsign.Parse("DL9XYZ")).Select(l => l.Call.Text).ToList());
QtcCounts counts = reopened.CountsFor(Callsign.Parse("DL9XYZ"));
Assert.Equal(2, counts.Exchanged);
Assert.Equal(1, counts.Available);
Assert.Equal(1, counts.LeftWith);
}
/// A QTC row holds the serial of the contact it reports, which the other
/// station sent. Taking that for our own next number would jump the log
/// forward; N1MM leaves the QTC rows out of the same query.
[Fact]
public void TrafficDoesNotTakeTheNextSerialNumber()
{
OperatingPosition first = Open();
Contact(first, "DL1AAA", receivedNumber: 1_450, minute: 40);
Contact(first, "DL2BBB", receivedNumber: 1_800, minute: 41);
Send(first, new QtcTraffic(first).ToSend(Callsign.Parse("DL9XYZ")));
Assert.Equal(3, first.SentNumber);
Assert.Equal(3, Open().SentNumber);
}
}