Files
Nonemm/tests/Nonemm.Formats.Tests/CabrilloWriterTests.cs
T
erikbrockoandClaude Sonnet 5.5 0ad894bfae Write LOCATION to Cabrillo, and give CQ WW M/2 eight band changes an hour
CQ WW lists LOCATION as required, and the export never wrote it. It now
carries the ARRL section, state or province, or DX.

CQ WW M/2 has no stay on a band: it may make eight band changes in a clock
hour. It was getting the ten-minute stay that M/S has.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 14:18:42 +00:00

315 lines
11 KiB
C#

using Nonemm.Contests;
using Nonemm.Contests.Rules;
using Nonemm.Contests.Udc;
using Nonemm.Core;
using Nonemm.Formats.Cabrillo;
namespace Nonemm.Formats.Tests;
public class CabrilloWriterTests
{
private static readonly StationInfo Me = new()
{
Callsign = "DL1ABC",
CqZone = 14,
Continent = "EU",
CountryPrefix = "DL",
};
private static Qso Contact(double kilohertz = 14_025, Mode? mode = null) => new()
{
Id = Qso.NewId(),
TimestampUtc = new DateTime(2026, 5, 30, 12, 34, 56, DateTimeKind.Utc),
Call = Callsign.Parse("JA1XYZ"),
Frequency = Frequency.FromKilohertz(kilohertz),
Mode = mode ?? Modes.Cw,
ContestName = "CQWW",
SentReport = "599",
ReceivedReport = "599",
Zone = 25,
SentNumber = 12,
ReceivedNumber = 34,
};
[Fact]
public void QsoLineCarriesBothExchanges()
{
CabrilloWriter writer = new(new CqWorldWide(ModeCategory.Cw), Me);
string line = writer.QsoLine(Contact());
Assert.StartsWith("QSO: 14025 CW 2026-05-30 1234 DL1ABC", line);
Assert.Contains("JA1XYZ", line);
Assert.EndsWith("0", line);
Assert.Contains(" 14 ", line);
Assert.Contains(" 25 ", line);
}
[Fact]
public void WpxNumbersArePaddedToFour()
{
CabrilloWriter writer = new(new CqWpx(ModeCategory.Cw), Me);
string line = writer.QsoLine(Contact());
Assert.Contains("0012", line);
Assert.Contains("0034", line);
}
[Fact]
public void VhfContactsUseTheBandDesignator()
{
CabrilloWriter writer = new(new CqWorldWide(ModeCategory.Cw), Me);
Assert.Contains(" 144 ", writer.QsoLine(Contact(144_200)));
Assert.Contains(" 1.2G ", writer.QsoLine(Contact(1_296_100)));
}
[Fact]
public void HeaderAndFooterWrapTheContacts()
{
CabrilloWriter writer = new(new CqWorldWide(ModeCategory.Cw), Me);
string log = writer.Write(
new CabrilloHeader { Contest = "CQ-WW-CW", Callsign = "DL1ABC", ClaimedScore = 1234 },
[Contact()]);
Assert.StartsWith("START-OF-LOG: 3.0\r\n", log);
Assert.Contains("CONTEST: CQ-WW-CW\r\n", log);
Assert.Contains("CLAIMED-SCORE: 1234\r\n", log);
Assert.Contains("LOCATION: DX\r\n", writer.Write(
new CabrilloHeader { Contest = "CQ-WW-CW", Callsign = "DL1ABC", Location = "DX" }, []));
Assert.EndsWith("END-OF-LOG:\r\n", log);
}
/// N1MM writes a contact the operator left out of the claimed score under
/// `X-QSO:`, which the sponsors read as a contact the entrant is not
/// claiming. It is not dropped from the file.
[Fact]
public void ContactsLeftOutOfTheClaimedScoreAreSentAsXQso()
{
CabrilloWriter writer = new(new CqWorldWide(ModeCategory.Cw), Me);
string log = writer.Write(
new CabrilloHeader { Contest = "CQ-WW-CW", Callsign = "DL1ABC" },
[Contact() with { IsClaimed = false }]);
Assert.Contains("\r\nX-QSO: ", log);
Assert.DoesNotContain("\r\nQSO:", log);
}
/// The three contests where a DX entrant sends a serial. N1MM writes the
/// serial of that contact, so a log cannot come out with `001` on every
/// line. ARRL 10M sends it unpadded, the other two pad it to three digits.
[Fact]
public void ADxEntrantSendsTheSerialOfTheContact()
{
Qso qso = Contact(28_025) with { SentNumber = 42 };
Assert.Contains(
"599 42 JA1XYZ",
new CabrilloWriter(new Arrl10Metre(), Me).QsoLine(qso));
Assert.Contains(
"599 042 JA1XYZ",
new CabrilloWriter(new UkrainianDx(), Me).QsoLine(qso));
Assert.Contains(
"599 042 JA1XYZ",
new CabrilloWriter(new YoDxHf(), Me).QsoLine(qso));
}
private static string WaeLine(string tag, string exchange) =>
tag.PadRight(7) + " 3799 PH 2026-05-30 1234 " + exchange;
/// WAE writes contacts and QTC traffic under different tags and in
/// different columns. N1MM's layouts are in `CWAE.CabrilloString`: a
/// contact leaves three spaces after the sent serial, and a QTC line is
/// the receiver, the series, the sender, then the time, callsign and
/// serial number of the contact being reported.
[Fact]
public void WaeWritesContactsAndTrafficUnderTheirOwnTags()
{
CabrilloWriter writer = new(new Wae(ModeCategory.Phone), Me);
Qso contact = Contact(3_799, Modes.Usb) with { SentNumber = 12, ReceivedNumber = 34 };
Assert.Equal(
WaeLine("QSO:", "DL1ABC 599 0012 JA1XYZ 599 0034 "),
writer.QsoLine(contact));
Qso sent = new WaeQtc(
Callsign.Parse("JA1XYZ"),
IsSent: true,
Series: 3,
CountInSeries: 10,
TimeUtc: "1201",
Call: Callsign.Parse("G3ABC"),
Number: 45).ApplyTo(contact);
Assert.Equal(
WaeLine("QTC:", "JA1XYZ 3/10 DL1ABC 1201 G3ABC 0045 "),
writer.QsoLine(sent));
Qso received = sent with { Exchange1 = WaeQtc.Received };
Assert.Equal(
WaeLine("QTC:", "DL1ABC 3/10 JA1XYZ 1201 G3ABC 0045 "),
writer.QsoLine(received));
}
/// The SA 10 file, whose sponsor asks for its own columns: the operator's
/// call in the first 13, then every column padded to the width the file
/// gives, with one space after it.
[Fact]
public void AUserDefinedContestCanLayOutItsOwnLine()
{
UserDefinedContest contest = new(UdcFile.Parse("""
[Contest]
Name=SA10_DX
CabrilloName=SA10-DX
CabrilloFormat=99
CabrilloString=SNT, 4, SentExch, 4, CallSign, 13, RCV, 4, Exchange1, 4
"""));
CabrilloWriter writer = new(contest, Me, new ContestEntry { SentExchange = "14" });
string line = writer.QsoLine(Contact() with { Exchange1 = "25" });
Assert.Equal(
"QSO: 14025 CW 2026-05-30 1234 DL1ABC 599 14 JA1XYZ 599 25 ",
line);
}
/// A value longer than its column is cut rather than pushing the columns
/// after it out of line, which is what N1MM writes.
[Fact]
public void AValueTooLongForItsColumnIsCut()
{
UserDefinedContest contest = new(UdcFile.Parse("""
[Contest]
Name=TIGHT
CabrilloString=Exchange1, 3
"""));
CabrilloWriter writer = new(contest, Me);
Assert.EndsWith("ABC ", writer.QsoLine(Contact() with { Exchange1 = "ABCDEF" }));
}
/// BARTG writes the time this station sent, which is the `MiscText`
/// column, and the time the other station sent in the exchange column.
[Fact]
public void TheBartgLineCarriesBothTimes()
{
CabrilloWriter writer = new(new BartgSprint(), Me);
string line = writer.QsoLine(
Contact(14_025, Modes.Rtty) with { MiscText = "1704", Exchange1 = "905" });
Assert.Equal(
"QSO: 14025 RY 2026-05-30 1234 "
+ "DL1ABC 599 0012 1704 "
+ "JA1XYZ 599 0034 0905 0",
line);
}
/// A contact logged before the time was stamped falls back to the time of
/// the contact, as N1MM does.
[Fact]
public void TheBartgLineFallsBackToTheTimeOfTheContact()
{
CabrilloWriter writer = new(new BartgSprint(), Me);
Assert.Contains("1234 JA1XYZ", writer.QsoLine(Contact(14_025, Modes.Rtty)));
}
/// `CabrilloFormat = 2` is the NAQP line: the serial number and the two
/// exchange values, each in a column of its own.
[Fact]
public void ANumberedCabrilloFormatLaysOutTheNaqpLine()
{
UserDefinedContest contest = new(UdcFile.Parse("""
[Contest]
Name=NAQP_TEST
CabrilloFormat=2
EntryWindowInfo=NameText, 500, Exchange1Text, 500
FrameText=Name QTH
"""));
CabrilloWriter writer = new(contest, Me, new ContestEntry { SentExchange = "JOE OH" });
string line = writer.QsoLine(Contact() with { Name = "SAM", Exchange1 = "OH" });
Assert.Equal(
"QSO: 14025 CW 2026-05-30 1234 "
+ "DL1ABC 12 JOE OH "
+ "JA1XYZ 34 SAM OH 0",
line);
}
/// `CabrilloFormat = 3` is the NA Sprint line, which steps over the serial
/// number in each exchange because the column before it holds it.
[Fact]
public void TheSprintLineStepsOverTheSerialNumberInTheExchange()
{
UserDefinedContest contest = new(UdcFile.Parse("""
[Contest]
Name=SPRINT_TEST
CabrilloFormat=3
EntryWindowInfo=RcvNrText, 500, NameText, 500, Exchange1Text, 500
FrameText=Nr Name QTH
"""));
CabrilloWriter writer = new(contest, Me, new ContestEntry { SentExchange = "001 JOE OH" });
string line = writer.QsoLine(Contact() with { Name = "SAM", Exchange1 = "OH" });
Assert.Equal(
"QSO: 14025 CW 2026-05-30 1234 "
+ "DL1ABC 12 JOE OH "
+ "JA1XYZ 34 SAM OH 0",
line);
}
/// `CabrilloFormat = 4` is the Sweepstakes line: the whole exchange in one
/// column after each callsign, and no transmitter column.
[Fact]
public void TheSweepstakesLineHoldsTheWholeExchangeInOneColumn()
{
UserDefinedContest contest = new(UdcFile.Parse("""
[Contest]
Name=SS_TEST
CabrilloFormat=4
EntryWindowInfo=RcvNrText, 500, PrecText, 500, CheckText, 500, SectionText, 500
"""));
CabrilloWriter writer = new(contest, Me, new ContestEntry { SentExchange = "A 55 OH" });
string line = writer.QsoLine(
Contact() with { Precedence = "B", Check = 77, Section = "OH" });
Assert.Equal(
"QSO: 14025 CW 2026-05-30 1234 "
+ "DL1ABC 0012 A 55 OH "
+ "JA1XYZ 0034 B 77 OH ",
line);
}
/// `CabrilloFormat = 5` writes the section and the serial number as one
/// value, without a space between them.
[Fact]
public void TheSectionAndSerialLineJoinsTheTwoValues()
{
UserDefinedContest contest = new(UdcFile.Parse("""
[Contest]
Name=RFC_TEST
CabrilloFormat=5
EntryWindowInfo=SectionText, 500, RcvNrText, 500
"""));
CabrilloWriter writer = new(contest, Me, new ContestEntry { SentExchange = "OH" });
string line = writer.QsoLine(Contact() with { Section = "OH" });
Assert.Equal(
"QSO: 14025 CW 2026-05-30 1234 "
+ "DL1ABC OH012 "
+ "JA1XYZ OH034 ",
line);
}
[Fact]
public void TheCabrilloVersionComesFromTheContest()
{
UserDefinedContest contest = new(UdcFile.Parse("""
[Contest]
Name=OLD
CabrilloVersion=2.0
"""));
CabrilloWriter writer = new(contest, Me);
Assert.StartsWith(
"START-OF-LOG: 2.0",
writer.Write(new CabrilloHeader { Contest = "OLD", Callsign = "DL1ABC" }, []));
}
}