Files
Nonemm/tests/Nonemm.Network.Tests/BroadcastTests.cs
T
erikbrockoandClaude Sonnet 5.5 072c04883e Mirror a radio from another computer's broadcast data
Listen for Radio broadcast data from another Nonemm or N1MM computer and
record its frequency, mode and split as this computer's own radio. No
radio is controlled. The settings are in the Configure Radios dialog.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 05:16:27 +00:00

241 lines
9.0 KiB
C#

using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Xml.Linq;
namespace Nonemm.Network.Tests;
public class BroadcastTests
{
[Fact]
public void An_address_without_a_port_takes_the_default_port()
{
IReadOnlyList<IPEndPoint>? parsed = BroadcastTargets.Parse("127.0.0.1 192.168.1.255:9000", 12060, out _);
Assert.Equal([new IPEndPoint(IPAddress.Loopback, 12060), new IPEndPoint(IPAddress.Parse("192.168.1.255"), 9000)], parsed);
}
[Theory]
[InlineData("not-an-address")]
[InlineData("127.0.0.1:99999")]
[InlineData("127.0.0.1:12060:1")]
public void An_entry_that_is_not_an_address_is_refused(string text)
{
Assert.Null(BroadcastTargets.Parse(text, 12060, out string problem));
Assert.Contains(text, problem);
}
[Fact]
public void The_score_and_the_rotor_have_their_own_default_ports()
{
Assert.Equal(12050, BroadcastTargets.DefaultFor(BroadcastKind.Score).Port);
Assert.Equal(12040, BroadcastTargets.DefaultFor(BroadcastKind.Rotor).Port);
Assert.Equal(12060, BroadcastTargets.DefaultFor(BroadcastKind.Radio).Port);
}
[Fact]
public void Radio_info_carries_the_frequency_in_units_of_ten_hertz()
{
string xml = BroadcastMessages.Radio(
new RadioReport(1, 14_025_500, 14_025_500, "CW", "OK1ABC", "OK1ABC", true, 1, false, 1, false, "", true),
"RUN-PC");
XElement root = XElement.Parse(xml);
Assert.Equal("RadioInfo", root.Name.LocalName);
Assert.Equal("1402550", root.Element("Freq")!.Value);
Assert.Equal("True", root.Element("IsRunning")!.Value);
Assert.Equal("RUN-PC", root.Element("StationName")!.Value);
}
[Fact]
public void A_radio_message_is_read_back_in_hertz()
{
string xml = BroadcastMessages.Radio(
new RadioReport(2, 14_025_500, 14_030_000, "USB", "OK1ABC", "OK1ABC", true, 2, true, 2, false, "", true),
"RUN-PC");
RadioBroadcast? read = BroadcastMessages.ReadRadio(xml);
Assert.Equal("RUN-PC", read!.StationName);
Assert.Equal(2, read.Radio.RadioNumber);
Assert.Equal(14_025_500, read.Radio.Frequency);
Assert.Equal(14_030_000, read.Radio.TransmitFrequency);
Assert.Equal("USB", read.Radio.Mode);
Assert.True(read.Radio.IsSplit);
}
[Theory]
[InlineData("not xml")]
[InlineData("<spot><dxcall>DL1ABC</dxcall></spot>")]
[InlineData("<RadioInfo><RadioNr>1</RadioNr></RadioInfo>")]
public void Anything_but_a_radio_message_is_passed_over(string text)
{
Assert.Null(BroadcastMessages.ReadRadio(text));
}
[Fact]
public void A_deleted_spot_carries_no_comment_or_status()
{
SpotReport spot = new("DL1ABC", 7_012_300, "OK2XYZ", new DateTime(2026, 5, 30, 12, 0, 0), "CW", "up 1", "new qso");
XElement added = XElement.Parse(BroadcastMessages.Spot(spot, "add", "RUN-PC"));
XElement deleted = XElement.Parse(BroadcastMessages.Spot(spot, "delete", "RUN-PC"));
Assert.Equal("7012.3", added.Element("frequency")!.Value);
Assert.Equal("new qso", added.Element("status")!.Value);
Assert.Null(deleted.Element("status"));
Assert.Null(deleted.Element("comment"));
}
private static ScoreReport Score(string state = "", string grid = "JO70", string assisted = "NON-ASSISTED") => new(
"CQ-WW-CW", "OK1ABC ", "OK1ABC", "HIGH", assisted, "ONE", "SINGLE-OP-ASSISTED", "ALL", "CW", "", "",
"", "OK", 15, 28, "", state, grid,
["ZN", "COUNTRYPREFIX"],
[
new ScoreReportRow(3.5, "CW", 7, [2, 3], 18),
new ScoreReportRow(ScoreLabels.TotalBand, "Both", 10, [3, 4], 25),
],
175, new DateTime(2026, 5, 30, 12, 0, 0));
[Fact]
public void The_score_is_written_in_the_layout_N1MM_uses()
{
string xml = BroadcastMessages.Score(Score(state: "OK"));
Assert.Equal(
"<?xml version=\"1.0\"?>\r\n<dynamicresults>\r\n"
+ "\t<contest>CQ-WW-CW</contest>\r\n"
+ "\t<call>OK1ABC</call>\r\n"
+ "\t<ops>OK1ABC</ops>\r\n"
+ "\t<class power=\"HIGH\" assisted = \"NON-ASSISTED\" transmitter=\"ONE\" ops=\"SINGLE-OP\" bands=\"ALL\" mode=\"CW\" overlay=\"\" station=\"\" ></class>\r\n"
+ "\t<club></club>\r\n"
+ "\t<qth>\r\n"
+ "\t\t<dxcccountry>OK</dxcccountry>\r\n"
+ "\t\t<cqzone>15</cqzone>\r\n"
+ "\t\t<iaruzone>28</iaruzone>\r\n"
+ "\t\t<stprvoth>OK</stprvoth>\r\n"
+ "\t\t<grid4>JO70</grid4>\r\n"
+ "\t</qth>\r\n"
+ "\t<breakdown>\r\n"
+ "\t\t<qso band=\"80\" mode=\"CW\">7</qso>\r\n"
+ "\t\t<mult band=\"80\" mode=\"CW\" type=\"zone\">2</mult>\r\n"
+ "\t\t<mult band=\"80\" mode=\"CW\" type=\"country\">3</mult>\r\n"
+ "\t\t<point band=\"80\" mode=\"CW\">18</point>\r\n"
+ "\t\t<qso band=\"total\" mode=\"ALL\">10</qso>\r\n"
+ "\t\t<mult band=\"total\" mode=\"ALL\" type=\"zone\">3</mult>\r\n"
+ "\t\t<mult band=\"total\" mode=\"ALL\" type=\"country\">4</mult>\r\n"
+ "\t\t<point band=\"total\" mode=\"ALL\">25</point>\r\n"
+ "\t</breakdown>\r\n"
+ "\t<score>175</score>\r\n"
+ "\t<timestamp>2026-05-30 12:00:00</timestamp>\r\n"
+ "</dynamicresults>\r\n",
xml);
}
[Fact]
public void The_score_leaves_out_an_empty_state_and_a_grid_that_is_not_four_or_six_letters()
{
string xml = BroadcastMessages.Score(Score(grid: "JO"));
Assert.DoesNotContain("stprvoth", xml);
Assert.DoesNotContain("grid", xml);
}
[Theory]
[InlineData(1.8, "160")]
[InlineData(3.5, "80")]
[InlineData(5, "60")]
[InlineData(7, "40")]
[InlineData(10, "30")]
[InlineData(14, "20")]
[InlineData(18, "17")]
[InlineData(21, "15")]
[InlineData(24, "12")]
[InlineData(28, "10")]
[InlineData(50, "6")]
[InlineData(70, "4")]
[InlineData(144, "2")]
[InlineData(222, "1.25")]
[InlineData(420, "0.7")]
[InlineData(902, "0.33")]
[InlineData(1240, "0.23")]
[InlineData(2300, "0.13")]
[InlineData(3300, "0.09")]
[InlineData(5650, "0.06")]
[InlineData(10000, "0.03")]
[InlineData(24000, "0.01")]
[InlineData(47000, "0.0064")]
[InlineData(76000, "0.004")]
[InlineData(122250, "0.0025")]
[InlineData(134000, "0.002")]
[InlineData(241000, "0.0012")]
[InlineData(0.1, "0")]
[InlineData(0.5, "0")]
[InlineData(ScoreLabels.TotalBand, "total")]
public void A_band_is_named_by_its_wavelength_in_metres(double band, string label)
{
Assert.Equal(label, ScoreLabels.Band(band));
}
[Theory]
[InlineData("USB", "PH")]
[InlineData("LSB", "PH")]
[InlineData("SSB", "PH")]
[InlineData("FM", "PH")]
[InlineData("CW", "CW")]
[InlineData("RCW", "CW")]
[InlineData("RTTY", "RTTY")]
[InlineData("RTTY-R", "RTTY")]
[InlineData("FT8", "FT8")]
[InlineData("PSK31", "PSK31")]
[InlineData("JT65", "ALL")]
[InlineData("Both", "ALL")]
public void A_mode_is_folded_into_the_few_N1MM_names(string mode, string label)
{
Assert.Equal(label, ScoreLabels.Mode(mode));
}
[Theory]
[InlineData("CountryPrefix", "country")]
[InlineData("Countryprefix", "country")]
[InlineData("Sect", "state")]
[InlineData("ZN", "zone")]
[InlineData("WPXPrefix", "WPXPREFIX")]
public void Three_multiplier_types_are_renamed_and_the_rest_are_upper_case(string name, string type)
{
Assert.Equal(type, ScoreLabels.MultiplierType(name));
}
[Fact]
public async Task A_kind_that_is_on_reaches_its_address_and_a_kind_that_is_off_does_not()
{
using UdpClient receiver = new(new IPEndPoint(IPAddress.Loopback, 0));
int port = ((IPEndPoint)receiver.Client.LocalEndPoint!).Port;
using DataBroadcaster broadcaster = new(new Dictionary<BroadcastKind, BroadcastChannel>
{
[BroadcastKind.Radio] = new() { IsEnabled = true, Addresses = [$"127.0.0.1:{port}"] },
[BroadcastKind.Spots] = new() { Addresses = [$"127.0.0.1:{port}"] },
});
broadcaster.Send(BroadcastKind.Spots, "<spot/>");
broadcaster.Send(BroadcastKind.Radio, "<RadioInfo/>");
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(5));
UdpReceiveResult received = await receiver.ReceiveAsync(timeout.Token);
Assert.Equal("<RadioInfo/>", Encoding.UTF8.GetString(received.Buffer));
Assert.False(broadcaster.IsSending(BroadcastKind.Spots));
}
[Fact]
public void A_row_with_an_unreadable_address_is_left_off_and_reported()
{
using DataBroadcaster broadcaster = new(new Dictionary<BroadcastKind, BroadcastChannel>
{
[BroadcastKind.Score] = new() { IsEnabled = true, Addresses = ["nowhere"] },
});
Assert.False(broadcaster.IsSending(BroadcastKind.Score));
Assert.Single(broadcaster.Problems);
}
}