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.EndsWith("END-OF-LOG:\r\n", log); } [Fact] public void ContactsLeftOutOfTheClaimedScoreAreNotSent() { 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.DoesNotContain("QSO:", log); } /// 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" }, [])); } }