Files
Nonemm/tests/Nonemm.Session.Tests/QtcTrafficTests.cs
ericek111 1c118f9ce4 Lay the menu bar out the way N1MM lays it out, and fill in the Edit menu
The menu tree comes from N1MM's EntryWindow.Designer.cs: File, Edit, View,
Tools, Config, Window, with the items under the names N1MM gives them. The
window list moved out of View into Window; the call history file moved from
Config into File > Import. Ctrl+W, Ctrl+L and Ctrl+M now work, so the keys
printed in the menu are real. Two commands that had no menu item before:
the CW and SSB function key definitions, and the QTC setup area.

The rest of the Edit menu:

- Ctrl+N asks for a note and puts it in the comment of the contact in the
  boxes, or of the last logged contact when nothing is typed.
- Edit Current Contact opens the Edit Contact form over what is typed but
  not logged. Update hands the values back to the entry boxes.
- Ctrl+Q and Ctrl+A load a logged contact into the boxes and paint them
  pale yellow. Enter writes the change back, Esc puts back what was being
  typed, and stepping forward past the newest contact leaves quick edit.
- Ctrl+U raises the received serial, or a numeric exchange box when the
  contest has no serial.
- Ctrl+F shows the call being typed in the log window, then the next
  contact with that call.

RadioPosition is now OperatingPosition: it is the operator at one radio,
not a place on a band.

Looked at under Xvfb on a 3775-contact log: quick edit back and forward,
Esc, the note dialog, find, and Edit Current Contact putting a zone back
into the entry boxes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 12:18:37 +00:00

211 lines
7.4 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));
}