Files
Nonemm/tests/Nonemm.Session.Tests/QtcTrafficTests.cs
T
erikbrockoandClaude Opus 5 7ca2c5cd52 Walk the QTC window the way N1MM walks it
Four changes to the QTC window, three of them N1MM's behaviour and one an
addition.

Tab, enter and space all move to the next box the operator types in, and shift
and tab move back. The walk covers those boxes and nothing else. N1MM handles
the three keys in every box itself — `QTCLinesA_KeyDown` and its two siblings,
with `PreviewKeyDown` claiming Tab — rather than leaving them to the tab order,
so the cursor never lands on a switched-off line or on a button beside one.
Tab was going to Avalonia's default walk, which covered every control in the
window.

The header now says how many lines take traffic. `QTCHeader_LostFocus` reads
the count after the last `/` and hands it to `ColorizeCWRXFields`, which enables
that many lines and switches off the rest, capped by how many of the station's
ten are still free. All ten were live whatever the header said. Nothing already
typed is cleared, which N1MM does under `DontErase`.

A time typed short is filled in from the line above as the cursor leaves the
box: `52` under `1148` reads as `1152`. `QTCLinesA_LostFocus` takes as many
characters off the front of the line above as the typed part is missing, and
drops a colon whatever the length. `QtcTraffic.FillTime` is that rule.

The addition: while the cursor is in a QTC callsign box the check window
answers for that call rather than the entry window's, so a call taken down in
traffic is checked the same way one being worked is. `CheckWindowSources`
takes the call from `Elsewhere` when the QTC window has set it.

Checked by driving the window: header `4/5` left five lines live and read
"1 of 5", tab walked header to time to call to serial to the next line's time,
shift and tab walked back, `52` on the second line became `1152`, and the check
window listed W1AW while `W1A` was typed in a QTC line.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015NZkFxHde1iifAQemFTHxf
2026-09-09 21:17:03 +00:00

234 lines
8.3 KiB
C#

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 (OperatingPosition 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));
OperatingPosition position = new(session);
return (position, new QtcTraffic(position));
}
private static Qso Contact(
OperatingPosition 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()
{
(OperatingPosition 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()
{
(OperatingPosition 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()
{
(OperatingPosition 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()
{
(OperatingPosition 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));
/// A time typed short takes the front off the line above, as many
/// characters as it is missing.
[Theory]
[InlineData("52", "1148", "1152")]
[InlineData("2", "1148", "1142")]
[InlineData("152", "1148", "1152")]
[InlineData("1207", "1148", "1207")]
[InlineData("12:07", "1148", "1207")]
public void ATimeTypedShortIsFilledInFromTheLineAbove(
string typed,
string above,
string filled) =>
Assert.Equal(filled, QtcTraffic.FillTime(typed, above));
/// The first line has nothing above it, so what was typed stands and is
/// judged as it is.
[Theory]
[InlineData("52", "52")]
[InlineData("", "")]
[InlineData(" 1148 ", "1148")]
public void ATimeOnTheFirstLineIsLeftAlone(string typed, string filled) =>
Assert.Equal(filled, QtcTraffic.FillTime(typed, ""));
}