Skip to content

Commit dc8126f

Browse files
committed
Add unit tests and configure GitHub Actions CI
Introduces a comprehensive suite of unit tests for TCP, UDP, Serial, and ProjectorLink components. This change also includes a CI workflow to run tests on pull requests, modernizes NetBooterLink to use HttpClient, and fixes a potential stack overflow in AsyncTcpLink during synchronous socket completions.
1 parent 1824d8b commit dc8126f

26 files changed

Lines changed: 1139 additions & 51 deletions

.github/workflows/run-tests.yml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
name: Run Unit Tests
2+
3+
on:
4+
pull_request:
5+
branches:
6+
- main
7+
types:
8+
- opened
9+
- synchronize
10+
- reopened
11+
12+
jobs:
13+
test:
14+
name: Build and run unit tests
15+
runs-on: ubuntu-latest
16+
steps:
17+
- name: GitHub actions Workspace Cleaner
18+
uses: jstone28/runner-workspace-cleaner@v1.0.0
19+
20+
- name: Checkout
21+
uses: actions/checkout@v4
22+
23+
- name: Setup .NET
24+
uses: actions/setup-dotnet@v4
25+
with:
26+
dotnet-version: "10.0.x"
27+
28+
- name: Restore dependencies
29+
run: dotnet restore
30+
working-directory: ./ThreeByte.LinkLib
31+
32+
- name: Build
33+
run: dotnet build --configuration Release --no-restore
34+
working-directory: ./ThreeByte.LinkLib
35+
36+
- name: Run tests
37+
run: dotnet test --configuration Release --no-build --logger "trx;LogFileName=test-results.trx"
38+
working-directory: ./ThreeByte.LinkLib

ThreeByte.LinkLib/ThreeByte.LinkLib.NetBooter/NetBooterLink.cs

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Generic;
33
using System.ComponentModel;
44
using System.Net;
5+
using System.Net.Http;
56
using Microsoft.Extensions.Logging;
67
using ThreeByte.LinkLib.Shared.Logging;
78

@@ -11,12 +12,19 @@ public class NetBooterLink : INotifyPropertyChanged
1112
{
1213
private readonly string _ipAddress;
1314
private readonly ILogger _logger;
15+
private readonly HttpClient _httpClient;
1416
private readonly Dictionary<int, bool> _powerStates = new Dictionary<int, bool>();
1517

1618
public NetBooterLink(string ipAddress)
1719
{
1820
_logger = LogFactory.Create<NetBooterLink>();
1921
_ipAddress = ipAddress;
22+
23+
var handler = new HttpClientHandler
24+
{
25+
Credentials = new NetworkCredential("admin", "admin")
26+
};
27+
_httpClient = new HttpClient(handler);
2028
}
2129

2230
public bool this[int port]
@@ -39,10 +47,8 @@ public void Power(int outlet, bool state)
3947
{
4048
try
4149
{
42-
var c = new WebClient();
43-
c.Credentials = new NetworkCredential("admin", "admin");
4450
var commandUri = string.Format("http://{0}/cmd.cgi?$A3 {1} {2}", _ipAddress, outlet, state ? 1 : 0);
45-
var response = c.DownloadString(commandUri);
51+
var response = _httpClient.GetStringAsync(commandUri).GetAwaiter().GetResult();
4652
_logger.LogDebug("Response: {0}", response);
4753
}
4854
catch (Exception ex)
@@ -56,10 +62,8 @@ public void PollState()
5662
{
5763
try
5864
{
59-
var c = new WebClient();
60-
c.Credentials = new NetworkCredential("admin", "admin");
6165
var commandUri = string.Format("http://{0}/cmd.cgi?$A5", _ipAddress);
62-
var response = c.DownloadString(commandUri);
66+
var response = _httpClient.GetStringAsync(commandUri).GetAwaiter().GetResult();
6367

6468
// Expected response: xxxx,cccc,tttt
6569
// read right to left for each field, eg - 01 means port 1 is on
@@ -84,14 +88,6 @@ public void PollState()
8488
}
8589
}
8690

87-
private void NotifyPropertyChanged(string info)
88-
{
89-
if (PropertyChanged != null)
90-
{
91-
PropertyChanged(this, new PropertyChangedEventArgs(info));
92-
}
93-
}
94-
9591
private void HandleError(Exception ex, string message)
9692
{
9793
_logger.LogError(ex, message);

ThreeByte.LinkLib/ThreeByte.LinkLib.ProjectorLink/Commands/Command.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
{
33
public abstract class Command
44
{
5-
public delegate void CommandResultHandler(Command sender, CommandResponse response);
6-
75
protected CommandResponse _cmdResponse;
86

97
internal virtual string GetCommandString()

ThreeByte.LinkLib/ThreeByte.LinkLib.ProjectorLink/Projector.cs

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
using Microsoft.Extensions.Logging;
22
using System;
33
using System.Net.Sockets;
4-
using System.Security.Cryptography;
54
using System.Text;
5+
using System.Security.Cryptography;
66
using ThreeByte.LinkLib.ProjectorLink.Commands;
77
using ThreeByte.LinkLib.Shared.Logging;
88

@@ -235,16 +235,18 @@ private void CloseConnection()
235235

236236
private string GetMD5Hash(string input)
237237
{
238-
MD5CryptoServiceProvider cryptoProvider = new MD5CryptoServiceProvider();
239-
byte[] bs = Encoding.ASCII.GetBytes(input);
240-
byte[] hash = cryptoProvider.ComputeHash(bs);
241-
242-
string toRet = "";
243-
foreach (byte b in hash)
238+
using (var md5 = MD5.Create())
244239
{
245-
toRet += b.ToString("x2");
240+
byte[] bs = Encoding.ASCII.GetBytes(input);
241+
byte[] hash = md5.ComputeHash(bs);
242+
243+
StringBuilder sb = new StringBuilder(hash.Length * 2);
244+
foreach (byte b in hash)
245+
{
246+
sb.Append(b.ToString("x2"));
247+
}
248+
return sb.ToString();
246249
}
247-
return toRet;
248250
}
249251

250252
private void HandleError(Exception ex, string message)

ThreeByte.LinkLib/ThreeByte.LinkLib.ProjectorLink/ThreeByte.LinkLib.ProjectorLink.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
<None Include="..\icon.png" Pack="true" PackagePath="" />
1414
</ItemGroup>
1515

16+
<ItemGroup>
17+
<InternalsVisibleTo Include="ThreeByte.LinkLib.Tests" />
18+
</ItemGroup>
19+
1620
<ItemGroup>
1721
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.5" />
1822
</ItemGroup>

ThreeByte.LinkLib/ThreeByte.LinkLib.SerialLink/FramedSerialLink.cs

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,12 @@ public void SendMessage(string message)
7575
}
7676

7777
//Add the header and footer
78-
byte[] header = new byte[0];
78+
byte[] header = Array.Empty<byte>();
7979
if (SendFrame != null && SendFrame.Header != null)
8080
{
8181
header = SendFrame.Header;
8282
}
83-
byte[] footer = new byte[0];
83+
byte[] footer = Array.Empty<byte>();
8484
if (SendFrame != null && SendFrame.Footer != null)
8585
{
8686
footer = SendFrame.Footer;
@@ -91,21 +91,18 @@ public void SendMessage(string message)
9191
Encoding.UTF8.GetBytes(message, 0, message.Length, messageBytes, header.Length);
9292
footer.CopyTo(messageBytes, message.Length + header.Length);
9393

94-
if (_serialLink != null)
94+
try
9595
{
96-
try
97-
{
98-
_serialLink.SendData(messageBytes);
99-
}
100-
catch (ObjectDisposedException ode)
101-
{
102-
HandleError(ode, "Cannot send a message of disposed FramedSerialLink.");
103-
}
104-
catch (Exception ex)
105-
{
106-
//Also possible for the serial link to raise and UnauthorizedAccessException here
107-
HandleError(ex, "SendMessage error.");
108-
}
96+
_serialLink.SendData(messageBytes);
97+
}
98+
catch (ObjectDisposedException ode)
99+
{
100+
HandleError(ode, "Cannot send a message of disposed FramedSerialLink.");
101+
}
102+
catch (Exception ex)
103+
{
104+
//Also possible for the serial link to raise and UnauthorizedAccessException here
105+
HandleError(ex, "SendMessage error.");
109106
}
110107
}
111108

@@ -162,13 +159,13 @@ private void OnDataReceived(object? sender, EventArgs e)
162159
{
163160
bool hasNewData = false;
164161

165-
byte[] header = new byte[0];
162+
byte[] header = Array.Empty<byte>();
166163
if (ReceiveFrame != null && ReceiveFrame.Header != null)
167164
{
168165
header = ReceiveFrame.Header;
169166
}
170167

171-
byte[] footer = new byte[0];
168+
byte[] footer = Array.Empty<byte>();
172169
if (ReceiveFrame != null && ReceiveFrame.Footer != null)
173170
{
174171
footer = ReceiveFrame.Footer;

ThreeByte.LinkLib/ThreeByte.LinkLib.SerialLink/SerialLink.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ private void SafeConnect()
178178
_serialPort.Parity = _settings.Parity;
179179
_serialPort.DataBits = _settings.DataBits;
180180
_serialPort.StopBits = StopBits.One;
181-
_serialPort.DataReceived += new SerialDataReceivedEventHandler(OnDataReceived);
181+
_serialPort.DataReceived += OnDataReceived;
182182
}
183183

184184
if (!_isConnected)
Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using Microsoft.Extensions.Logging;
1+
using System;
2+
using Microsoft.Extensions.Logging;
23

34
namespace ThreeByte.LinkLib.Shared.Logging
45
{
@@ -7,11 +8,12 @@ namespace ThreeByte.LinkLib.Shared.Logging
78
/// </summary>
89
public class LogFactory
910
{
11+
private static readonly Lazy<ILoggerFactory> Factory = new Lazy<ILoggerFactory>(() =>
12+
LoggerFactory.Create(builder => { builder.AddConsole(); }));
13+
1014
public static ILogger Create<T>()
1115
{
12-
var factory = LoggerFactory.Create(builder => { builder.AddConsole(); });
13-
14-
return factory.CreateLogger<T>();
16+
return Factory.Value.CreateLogger<T>();
1517
}
1618
}
17-
}
19+
}

ThreeByte.LinkLib/ThreeByte.LinkLib.TcpLink/AsyncTcpLink.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Generic;
33
using System.Net.Sockets;
44
using System.Threading;
5+
using System.Threading.Tasks;
56
using Microsoft.Extensions.Logging;
67
using ThreeByte.LinkLib.Shared.Logging;
78

@@ -384,7 +385,17 @@ private void ReadCallback(IAsyncResult asyncResult)
384385
DataReceived(this, new EventArgs());
385386
}
386387

387-
ReceiveData();
388+
// When BeginRead completes synchronously the callback fires on the same
389+
// thread, so a direct call to ReceiveData would recurse on the same stack
390+
// and eventually overflow. Trampolining via the ThreadPool breaks the chain.
391+
if (asyncResult.CompletedSynchronously)
392+
{
393+
ThreadPool.QueueUserWorkItem(_ => ReceiveData());
394+
}
395+
else
396+
{
397+
ReceiveData();
398+
}
388399
}
389400

390401
/// <summary>

0 commit comments

Comments
 (0)