Take WAE QTC traffic down

Ctrl+Z opens the QTC window for the station being worked, which is the key N1MM
puts it on, and the window is laid out the way N1MM lays it out: the header,
ten lines of time, callsign and serial number, an Agn and a Cfm button beside
each, Clear, Close and Cancel underneath, and the same colours — green for a
line that will be saved, red for one still empty, yellow for one that cannot be
read. Enter and Tab move forward and space moves within a line, so a line can be
walked while listening to it.

Sending, the lines are filled from the log and cannot be edited: the contacts
not reported to anybody yet, oldest first, never the station being worked, and
never past the ten any station may have. The rule for what has been reported is
N1MM's — a contact counts as reported when a QTC row carries its call and the
serial we sent it — so the pair is kept in the row the way N1MM keeps it.

Which way traffic goes is not the operator's choice on CW and SSB: Europe
receives and the rest of the world sends. On RTTY it goes both ways, and there
the window carries a switch. That is the one piece of the workflow that is ours
rather than N1MM's, which toggles a QTC mode outside the window instead.

Nothing here transmits. N1MM sends the lines on CW and RTTY and plays recorded
voice messages on SSB; we have no QTC message templates, no voice keyer and no
digital window, so Agn, Cfm and RX Ready move the cursor and the operator sends
by hand.

Running it caught what the tests could not: focus set in the constructor does
not stick, so the first thing typed into a freshly opened window was dropped and
every field after it took the value of the one before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-28 00:02:47 +00:00
parent 8961677354
commit 9ae5078c84
11 changed files with 886 additions and 11 deletions

View File

@@ -0,0 +1,210 @@
using Nonemm.Contests;
using Nonemm.Contests.Rules;
using Nonemm.Core;
using Nonemm.Core.Country;
using Nonemm.Storage;
namespace Nonemm.Session.Tests;
public class QtcTrafficTests
{
private const string Countries = """
Germany: 14: 28: EU: 51.00: -10.00: -1.0: DL:
DL,DK,DJ;
Slovakia: 15: 28: EU: 48.70: -19.50: -1.0: OM:
OM;
United States: 05: 08: NA: 37.53: 91.67: 5.0: K:
K,W,N,AA,NE;
Japan: 25: 45: AS: 36.40: -138.38: -9.0: JA:
JA,JH,JR;
""";
private static readonly StationInfo Slovakia = new()
{
Callsign = "OM5M",
CqZone = 15,
ItuZone = 28,
Continent = "EU",
CountryPrefix = "OM",
};
private static readonly StationInfo UnitedStates = new()
{
Callsign = "NE1C",
CqZone = 5,
ItuZone = 8,
Continent = "NA",
CountryPrefix = "K",
};
private static (RadioPosition Position, QtcTraffic Traffic) Station(
ModeCategory mode,
StationInfo? me = null)
{
FakeLogStore store = new();
ContestInstance instance = store.AddContest(new ContestInstance
{
ContestNumber = 0,
ContestName = "WAERTTY",
});
ContestSession session = new(
store, new Wae(mode), instance, me ?? Slovakia, CountryFile.Parse(Countries));
RadioPosition position = new(session);
return (position, new QtcTraffic(position));
}
private static Qso Contact(
RadioPosition position,
string call,
int receivedNumber,
int sentNumber,
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_080),
Mode = Modes.Rtty,
ContestName = "WAERTTY",
ReceivedNumber = receivedNumber,
SentNumber = sentNumber,
Exchange1 = WaeQtc.Contact,
});
[Fact]
public void OnCwEuropeReceivesAndTheRestOfTheWorldSends()
{
(_, QtcTraffic european) = Station(ModeCategory.Cw);
(_, QtcTraffic american) = Station(ModeCategory.Cw, UnitedStates);
Assert.Equal(QtcDirection.Receive, european.DirectionFor(Callsign.Parse("NE1C"), out _));
Assert.Equal(QtcDirection.Send, american.DirectionFor(Callsign.Parse("DL9XYZ"), out _));
}
[Fact]
public void OnCwThereIsNoTrafficWithinEurope()
{
(_, QtcTraffic traffic) = Station(ModeCategory.Cw);
Assert.Equal(QtcDirection.None, traffic.DirectionFor(Callsign.Parse("DL9XYZ"), out string why));
Assert.Contains("Europe", why);
}
[Fact]
public void OnRttyTrafficGoesBothWaysBetweenContinents()
{
(_, QtcTraffic traffic) = Station(ModeCategory.Digital);
Assert.Equal(QtcDirection.Either, traffic.DirectionFor(Callsign.Parse("JA1XYZ"), out _));
}
[Fact]
public void OnRttyThereIsNoTrafficWithinOneContinent()
{
(_, QtcTraffic traffic) = Station(ModeCategory.Digital);
Assert.Equal(QtcDirection.None, traffic.DirectionFor(Callsign.Parse("DL9XYZ"), out string why));
Assert.Contains("continent", why);
}
[Fact]
public void TenLinesIsAllAnyStationGets()
{
(RadioPosition position, QtcTraffic traffic) = Station(ModeCategory.Digital);
for (int at = 1; at <= 10; at++)
{
position.Session.Add(new WaeQtc(
Callsign.Parse("JA1XYZ"), IsSent: true, 1, 10, "1300", Callsign.Parse("DL9XYZ"), at)
.ApplyTo(Contact(position, "K1ABC", at, at, at)));
}
Assert.Equal(0, traffic.Remaining(Callsign.Parse("JA1XYZ")));
Assert.Equal(QtcDirection.None, traffic.DirectionFor(Callsign.Parse("JA1XYZ"), out string why));
Assert.Contains("ten QTCs", why);
}
[Fact]
public void WhatIsSentIsTheContactsNotReportedYetOldestFirst()
{
(RadioPosition position, QtcTraffic traffic) = Station(ModeCategory.Digital);
Contact(position, "DL1AAA", receivedNumber: 16, sentNumber: 1, minute: 46);
Contact(position, "DL2BBB", receivedNumber: 40, sentNumber: 2, minute: 47);
Contact(position, "JA1XYZ", receivedNumber: 7, sentNumber: 3, minute: 48);
IReadOnlyList<WaeQtc> lines = traffic.ToSend(Callsign.Parse("JA1XYZ"));
Assert.Equal(["DL1AAA", "DL2BBB"], lines.Select(l => l.Call.Text).ToList());
Assert.Equal(["1346", "1347"], lines.Select(l => l.TimeUtc).ToList());
Assert.Equal([16, 40], lines.Select(l => l.Number).ToList());
Assert.All(lines, line => Assert.Equal("1/2", line.SeriesText));
}
[Fact]
public void AContactAlreadyReportedIsNotReportedAgain()
{
(RadioPosition position, QtcTraffic traffic) = Station(ModeCategory.Digital);
Contact(position, "DL1AAA", receivedNumber: 16, sentNumber: 1, minute: 46);
Contact(position, "DL2BBB", receivedNumber: 40, sentNumber: 2, minute: 47);
WaeQtc first = traffic.ToSend(Callsign.Parse("JA1XYZ"))[0];
position.Session.Add(first.ApplyTo(Contact(position, "JA1XYZ", 1, 3, 50)));
Assert.Equal(["DL2BBB"], traffic.ToSend(Callsign.Parse("JA1XYZ")).Select(l => l.Call.Text).ToList());
}
/// Series are numbered across the contest, so the next one carries on from
/// the last series sent to anybody.
[Fact]
public void SeriesAreNumberedAcrossTheContest()
{
(RadioPosition position, QtcTraffic traffic) = Station(ModeCategory.Digital);
Assert.Equal(1, traffic.NextSeries());
position.Session.Add(new WaeQtc(
Callsign.Parse("JA1XYZ"), IsSent: true, 86, 10, "1300", Callsign.Parse("DL9XYZ"), 5)
.ApplyTo(Contact(position, "JA1XYZ", 1, 1, 55)));
Assert.Equal(87, traffic.NextSeries());
}
[Theory]
[InlineData("1346", "DL2UH", "40")]
[InlineData("0001", "2E0GYI", "1")]
public void AWellFormedLineReadsAsAQtc(string time, string call, string number) =>
Assert.NotNull(QtcTraffic.Read(
Callsign.Parse("NE1C"), isSent: false, 87, 10, time, call, number));
[Theory]
[InlineData("134", "DL2UH", "40")]
[InlineData("2401", "DL2UH", "40")]
[InlineData("1360", "DL2UH", "40")]
[InlineData("1346", "DL", "40")]
[InlineData("1346", "DL2UH", "")]
[InlineData("1346", "DL2UH", "abc")]
public void ALineThatIsNotAQtcReadsAsNothing(string time, string call, string number) =>
Assert.Null(QtcTraffic.Read(
Callsign.Parse("NE1C"), isSent: false, 87, 10, time, call, number));
/// The header is copied off the air, so whatever separates the two numbers
/// is taken as the separator.
[Theory]
[InlineData("87/10")]
[InlineData("87 10")]
[InlineData("87-10")]
[InlineData("QTC 87/10")]
public void AHeaderIsASeriesAndACount(string text)
{
(int series, int count) = Assert.IsType<(int, int)>(QtcTraffic.ReadHeader(text));
Assert.Equal(87, series);
Assert.Equal(10, count);
}
[Theory]
[InlineData("")]
[InlineData("87")]
[InlineData("87/11")]
[InlineData("abc/10")]
public void AHeaderThatIsNotOneReadsAsNothing(string text) =>
Assert.Null(QtcTraffic.ReadHeader(text));
}