The log window took any text in the two report columns. It now checks the report the same way it checks the zone or the section: RST on most modes, and a signal-to-noise ratio in decibels on FT8, FT4, JT65, Q65 and MSK144. A WAE QTC row is skipped, because it keeps the time sent and the other station's call in those two columns. The grid column is checked on every contest now, not only when the contest asks for a locator, and the value is stored as JN88dd. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015NZkFxHde1iifAQemFTHxf
266 lines
9.2 KiB
C#
266 lines
9.2 KiB
C#
using Nonemm.Contests.Rules;
|
|
using Nonemm.Core;
|
|
using Nonemm.Core.Country;
|
|
|
|
namespace Nonemm.Session.Tests;
|
|
|
|
public class QsoEditorTests
|
|
{
|
|
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 QsoEditor CqWw() =>
|
|
new(new CqWorldWide(ModeCategory.Cw), Me, CountryFile.Parse(Countries));
|
|
|
|
private static QsoEditor Sweeps() =>
|
|
new(new Sweepstakes(ModeCategory.Cw), Me, CountryFile.Parse(Countries));
|
|
|
|
private static Qso Contact() => new()
|
|
{
|
|
Id = "1",
|
|
TimestampUtc = new DateTime(2026, 5, 30, 12, 34, 56, DateTimeKind.Utc),
|
|
Call = Callsign.Parse("JA1XYZ"),
|
|
Frequency = Frequency.FromKilohertz(14_025),
|
|
Mode = Modes.Cw,
|
|
ContestName = "CQWW",
|
|
SentReport = "599",
|
|
ReceivedReport = "599",
|
|
Zone = 25,
|
|
CountryPrefix = "JA",
|
|
Continent = "AS",
|
|
};
|
|
|
|
[Fact]
|
|
public void TheColumnsFollowTheContestExchange()
|
|
{
|
|
IReadOnlyList<QsoField> fields = [.. CqWw().Columns.Select(c => c.Field)];
|
|
|
|
Assert.Contains(QsoField.Zone, fields);
|
|
Assert.DoesNotContain(QsoField.Section, fields);
|
|
Assert.DoesNotContain(QsoField.SentNumber, fields);
|
|
}
|
|
|
|
/// Sweepstakes counts serial numbers, so the log shows the one we sent.
|
|
[Fact]
|
|
public void AContestWithSerialNumbersGetsASentNumberColumn() =>
|
|
Assert.Contains(QsoField.SentNumber, Sweeps().Columns.Select(c => c.Field));
|
|
|
|
/// Sweepstakes types the whole exchange in one box and logs it in four
|
|
/// columns, as N1MM does.
|
|
[Fact]
|
|
public void TheSweepstakesExchangeGetsAColumnPerPart()
|
|
{
|
|
IReadOnlyList<QsoField> fields = [.. Sweeps().Columns.Select(c => c.Field)];
|
|
|
|
Assert.Contains(QsoField.ReceivedNumber, fields);
|
|
Assert.Contains(QsoField.Precedence, fields);
|
|
Assert.Contains(QsoField.Check, fields);
|
|
Assert.Contains(QsoField.Section, fields);
|
|
Assert.DoesNotContain(QsoField.Exchange1, fields);
|
|
}
|
|
|
|
/// A WAE row says whether it is a contact or QTC traffic in the exchange
|
|
/// column — `QSO`, `SQTC` or `RQTC` — and N1MM's log has that column.
|
|
[Fact]
|
|
public void TheWaeLogHasAColumnForTheQtcDesignator()
|
|
{
|
|
QsoEditor editor = new(new Wae(ModeCategory.Cw), Me, CountryFile.Parse(Countries));
|
|
|
|
Assert.Equal(
|
|
["Series", "QTC?"],
|
|
editor.Columns
|
|
.Where(c => c.Field is QsoField.Exchange1 or QsoField.MiscText)
|
|
.Select(c => c.Label)
|
|
.ToList());
|
|
}
|
|
|
|
[Fact]
|
|
public void ChangingTheCallLooksTheCountryUpAgain()
|
|
{
|
|
QsoEdit edit = CqWw().Apply(Contact(), QsoField.Call, "dl2xyz");
|
|
|
|
Assert.True(edit.IsAccepted);
|
|
Assert.Equal("DL2XYZ", edit.Result!.Call.Text);
|
|
Assert.Equal("DL", edit.Result.CountryPrefix);
|
|
Assert.Equal("EU", edit.Result.Continent);
|
|
Assert.Equal("DL2", edit.Result.WpxPrefix);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("12345")]
|
|
[InlineData("AB")]
|
|
[InlineData("JA1XY Z")]
|
|
public void ATextThatIsNotShapedLikeACallIsRefused(string text)
|
|
{
|
|
QsoEdit edit = CqWw().Apply(Contact(), QsoField.Call, text);
|
|
|
|
Assert.False(edit.IsAccepted);
|
|
Assert.Contains("callsign", edit.Error);
|
|
}
|
|
|
|
[Fact]
|
|
public void ARefusedEditReturnsNoContact() =>
|
|
Assert.Null(CqWw().Apply(Contact(), QsoField.Zone, "41").Result);
|
|
|
|
[Theory]
|
|
[InlineData("40", true)]
|
|
[InlineData("1", true)]
|
|
[InlineData("41", false)]
|
|
[InlineData("0", false)]
|
|
[InlineData("two", false)]
|
|
public void CqZonesRunFromOneToForty(string text, bool accepted) =>
|
|
Assert.Equal(accepted, CqWw().Apply(Contact(), QsoField.Zone, text).IsAccepted);
|
|
|
|
[Fact]
|
|
public void AnUnknownSectionIsRefused() =>
|
|
Assert.False(Sweeps().Apply(Contact(), QsoField.Section, "ZZ").IsAccepted);
|
|
|
|
[Fact]
|
|
public void ASectionIsStoredUpperCase() =>
|
|
Assert.Equal("STX", Sweeps().Apply(Contact(), QsoField.Section, "stx").Result!.Section);
|
|
|
|
[Fact]
|
|
public void ACommentKeepsTheCaseItWasTypedIn() =>
|
|
Assert.Equal("Nice signal", CqWw().Apply(Contact(), QsoField.Comment, "Nice signal").Result!.Comment);
|
|
|
|
[Fact]
|
|
public void ClearingAFieldIsAllowed() =>
|
|
Assert.Equal(0, CqWw().Apply(Contact(), QsoField.Zone, "").Result!.Zone);
|
|
|
|
[Fact]
|
|
public void TheTimeIsReadAsUtc()
|
|
{
|
|
QsoEdit edit = CqWw().Apply(Contact(), QsoField.Time, "2026-05-30 13:00:00");
|
|
|
|
Assert.Equal(new DateTime(2026, 5, 30, 13, 0, 0, DateTimeKind.Utc), edit.Result!.TimestampUtc);
|
|
}
|
|
|
|
[Fact]
|
|
public void AFrequencyIsTypedInKilohertz() =>
|
|
Assert.Equal(
|
|
21_005_000,
|
|
CqWw().Apply(Contact(), QsoField.Frequency, "21005").Result!.Frequency.Hertz);
|
|
|
|
[Fact]
|
|
public void AModeTheLoggerDoesNotKnowIsRefused() =>
|
|
Assert.False(CqWw().Apply(Contact(), QsoField.Mode, "SSTV").IsAccepted);
|
|
|
|
[Fact]
|
|
public void AnEmptyNumberReadsBackAsAnEmptyColumn() =>
|
|
Assert.Equal("", QsoEditor.Read(Contact() with { Zone = 0 }, QsoField.Zone));
|
|
|
|
[Fact]
|
|
public void SeveralFieldsCanBeChangedAtOnce()
|
|
{
|
|
QsoEdit edit = CqWw().ApplyAll(Contact(),
|
|
[
|
|
new(QsoField.Call, "DL2XYZ"),
|
|
new(QsoField.Zone, "14"),
|
|
new(QsoField.Comment, "first QSO"),
|
|
]);
|
|
|
|
Assert.Equal("DL2XYZ", edit.Result!.Call.Text);
|
|
Assert.Equal(14, edit.Result.Zone);
|
|
Assert.Equal("first QSO", edit.Result.Comment);
|
|
Assert.Equal("DL", edit.Result.CountryPrefix);
|
|
}
|
|
|
|
[Fact]
|
|
public void TheFirstValueTheContestWillNotTakeStopsTheWholeChange()
|
|
{
|
|
QsoEdit edit = CqWw().ApplyAll(Contact(),
|
|
[
|
|
new(QsoField.Call, "DL2XYZ"),
|
|
new(QsoField.Zone, "41"),
|
|
]);
|
|
|
|
Assert.Null(edit.Result);
|
|
Assert.Equal(QsoField.Zone, edit.Field);
|
|
}
|
|
|
|
[Fact]
|
|
public void ARefusalNamesTheFieldItCameFrom() =>
|
|
Assert.Equal(QsoField.Call, CqWw().Apply(Contact(), QsoField.Call, "12345").Field);
|
|
|
|
[Fact]
|
|
public void AQsxFrequencyCanBeCleared() =>
|
|
Assert.Equal(
|
|
0,
|
|
CqWw().Apply(Contact() with { QsxFrequency = Frequency.FromKilohertz(14_200) },
|
|
QsoField.QsxFrequency, "").Result!.QsxFrequency.Hertz);
|
|
|
|
/// The working frequency is what the contact was made on, so it cannot be
|
|
/// emptied the way the QSX frequency can.
|
|
[Fact]
|
|
public void TheWorkingFrequencyCannotBeCleared() =>
|
|
Assert.False(CqWw().Apply(Contact(), QsoField.Frequency, "").IsAccepted);
|
|
|
|
[Fact]
|
|
public void ArbitraryTextInTheReportColumnIsRefused()
|
|
{
|
|
QsoEdit edit = CqWw().Apply(Contact(), QsoField.ReceivedReport, "good copy");
|
|
|
|
Assert.False(edit.IsAccepted);
|
|
Assert.Contains("signal report", edit.Error);
|
|
Assert.Equal(QsoField.ReceivedReport, edit.Field);
|
|
}
|
|
|
|
[Fact]
|
|
public void TheSentReportIsCheckedTheSameWay() =>
|
|
Assert.False(CqWw().Apply(Contact(), QsoField.SentReport, "699").IsAccepted);
|
|
|
|
[Fact]
|
|
public void AReportCanStillBeCorrected() =>
|
|
Assert.Equal("339", CqWw().Apply(Contact(), QsoField.ReceivedReport, "339").Result!.ReceivedReport);
|
|
|
|
/// FT8 exchanges a signal-to-noise ratio, so RST is the wrong thing there
|
|
/// and decibels are the wrong thing on CW.
|
|
[Fact]
|
|
public void AWeakSignalContactReportsDecibels()
|
|
{
|
|
Qso ft8 = Contact() with { Mode = Modes.Ft8, ReceivedReport = "-12" };
|
|
|
|
Assert.True(CqWw().Apply(ft8, QsoField.ReceivedReport, "-07").IsAccepted);
|
|
Assert.False(CqWw().Apply(ft8, QsoField.ReceivedReport, "599").IsAccepted);
|
|
Assert.False(CqWw().Apply(Contact(), QsoField.ReceivedReport, "-07").IsAccepted);
|
|
}
|
|
|
|
/// A WAE QTC row keeps the time sent and the other station's call in the
|
|
/// two report columns, so those are not reports to check.
|
|
[Fact]
|
|
public void TheReportColumnsOfAQtcRowAreLeftAlone()
|
|
{
|
|
QsoEditor editor = new(new Wae(ModeCategory.Cw), Me, CountryFile.Parse(Countries));
|
|
Qso traffic = Contact() with { ContestName = "WAEDC", Exchange1 = "SQTC", MiscText = "3/10" };
|
|
|
|
Assert.True(editor.Apply(traffic, QsoField.SentReport, "1234").IsAccepted);
|
|
Assert.True(editor.Apply(traffic, QsoField.ReceivedReport, "DL1ABC").IsAccepted);
|
|
}
|
|
|
|
[Fact]
|
|
public void TextThatIsNotALocatorIsRefused() =>
|
|
Assert.False(CqWw().Apply(Contact(), QsoField.GridSquare, "nowhere").IsAccepted);
|
|
|
|
/// A locator is stored the way it is written, whatever case it was typed in.
|
|
[Fact]
|
|
public void ALocatorIsStoredWithALowerCaseSubsquare() =>
|
|
Assert.Equal("JN88dd", CqWw().Apply(Contact(), QsoField.GridSquare, "jn88dd").Result!.GridSquare);
|
|
|
|
[Fact]
|
|
public void ACorrectedCountryPrefixIsKept() =>
|
|
Assert.Equal("DL", CqWw().Apply(Contact(), QsoField.CountryPrefix, "dl").Result!.CountryPrefix);
|
|
}
|