Add the Broadcast Data dialog and send N1MM's score breakdown
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
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_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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user