Files
Nonemm/tests/Nonemm.Network.Tests/BroadcastTests.cs
T

215 lines
8.1 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_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);
}
}