Skip to content

Commit 5f246d4

Browse files
author
LoneWandererProductions
committed
SimpleLogServer: Unit tests added
New file SimpleLogServerTests.cs added with unit tests for SimpleLogServer. The tests check the processing of messages via action callback and ILogProcessor, as well as the timeout behavior with slow clients. Contains a FakeLogProcessor helper class and uses asynchronous mechanisms for synchronization and error prevention when establishing connections.
1 parent 5c2cb7a commit 5f246d4

8 files changed

Lines changed: 259 additions & 11 deletions

File tree

CommonControls.Images/Thumbnails.xaml.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
using System.Windows.Input;
2727
using System.Windows.Media;
2828
using System.Windows.Media.Imaging;
29-
using Mathematics;
3029

3130
namespace CommonControls.Images
3231
{

Communication/LogCollectorServer.cs

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
* PROGRAMMER: Peter Geinitz (Wayfarer)
77
*/
88

9+
using System;
10+
using System.Diagnostics;
911
using System.IO;
1012
using System.Net.Sockets;
1113
using System.Text;
@@ -15,6 +17,7 @@
1517

1618
namespace Communication
1719
{
20+
/// <inheritdoc />
1821
/// <summary>
1922
/// Connects and listens for logs continuously.
2023
/// Central entry and processing point for logs, which can be extended to save logs to a database, write to files, or perform any other processing as needed.
@@ -36,27 +39,72 @@ public LogCollectorServer(int port, ILogProcessor processor) : base(port)
3639
_processor = processor;
3740
}
3841

42+
/// <inheritdoc />
3943
/// <summary>
4044
/// Handles the client asynchronous.
4145
/// </summary>
4246
/// <param name="client">The client.</param>
4347
/// <param name="token">The token.</param>
4448
protected override async Task HandleClientAsync(TcpClient client, CancellationToken token)
4549
{
50+
// 1. Set a hard timeout on the socket itself.
51+
// If no data arrives for 5 seconds, the socket throws an IOException.
52+
client.ReceiveTimeout = 5000;
53+
54+
// 2. Use a specific cancellation token source for just this client
55+
// capable of cancelling if the global token cancels OR if we time out manually.
56+
using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(token))
4657
using (var stream = client.GetStream())
4758
using (var reader = new StreamReader(stream, Encoding.UTF8))
4859
{
49-
string? line;
50-
while ((line = await reader.ReadLineAsync()) != null)
60+
try
5161
{
52-
if (token.IsCancellationRequested) break;
53-
54-
if (!string.IsNullOrWhiteSpace(line))
62+
while (!token.IsCancellationRequested)
5563
{
56-
await _processor.ProcessMessageAsync(line);
64+
// 3. The "Safe" Read.
65+
// StreamReader.ReadLineAsync() typically DOES NOT accept a CancellationToken directly
66+
// in older .NET versions, and it doesn't respect ReceiveTimeout easily.
67+
// We must handle the timeout manually or use a specific trick.
68+
69+
var lineTask = reader.ReadLineAsync();
70+
71+
// Wait for the line OR a timeout (e.g., 5 seconds idle)
72+
var timeoutTask = Task.Delay(TimeSpan.FromSeconds(5), linkedCts.Token);
73+
74+
var completedTask = await Task.WhenAny(lineTask, timeoutTask);
75+
76+
if (completedTask == timeoutTask)
77+
{
78+
// Timeout occurred! Client is too slow or holding the connection open.
79+
Trace.WriteLine("Client timed out. Disconnecting.");
80+
break;
81+
}
82+
83+
// If we got here, the line was read successfully
84+
string line = await lineTask;
85+
86+
if (line == null) break; // Client disconnected gracefully
87+
88+
if (!string.IsNullOrWhiteSpace(line))
89+
{
90+
await _processor.ProcessMessageAsync(line);
91+
}
5792
}
5893
}
94+
catch (IOException)
95+
{
96+
// Socket timeout or connection issue
97+
}
98+
catch (OperationCanceledException)
99+
{
100+
// Server is stopping
101+
}
102+
catch (Exception ex)
103+
{
104+
Trace.WriteLine($"Error processing client: {ex.Message}");
105+
}
59106
}
107+
60108
}
61109
}
62110
}

Communication/SimpleLogServer.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44
* FILE: SimpleLogServer.cs
55
* PURPOSE: Facade to simplify usage of LogCollectorServer for users who just want to provide a simple callback for log processing without needing to implement the ILogProcessor interface themselves.
66
* PROGRAMMER: Peter Geinitz (Wayfarer)
7+
* * TO DO:
8+
* 1. [Config] Expose the 'Read Timeout' (currently hardcoded to 5s) as a constructor parameter or options object.
9+
* 2. [Events] Add an 'OnError' Action<Exception> callback so users can handle server errors instead of just Console.WriteLine.
10+
* 3. [Events] Add 'OnClientConnected' and 'OnClientDisconnected' callbacks for better monitoring.
11+
* 4. [Security] Add support for SslStream (TLS) to allow secure log transmission.
12+
* 5. [Network] Explicitly support IPv6 (currently defaults to IPv4/Any).
713
*/
814

915
using System;
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
/*
2+
* COPYRIGHT: See COPYING in the top level directory
3+
* PROJECT: CommunicationTests
4+
* FILE: SimpleLogServerTests.cs
5+
* PURPOSE: Simple Log Server Tests
6+
* PROGRAMMER: Peter Geinitz (Wayfarer)
7+
*/
8+
9+
using System.Net.Sockets;
10+
using System.Text;
11+
using Communication;
12+
using Communication.Interfaces;
13+
14+
namespace CommunicationTests
15+
{
16+
[TestClass]
17+
public class SimpleLogServerTests
18+
{
19+
// Use a random high port to avoid conflicts
20+
private const int TestPort = 55123;
21+
22+
/// <summary>
23+
/// Actions the constructor should receive and process message.
24+
/// </summary>
25+
[TestMethod]
26+
public async Task ActionConstructor_ShouldReceiveAndProcessMessage()
27+
{
28+
// 1. Setup a TaskCompletionSource. This acts as our "Flag".
29+
// When the server gets a message, it sets this flag.
30+
var messageReceivedSignal = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
31+
32+
using (var server = new SimpleLogServer(TestPort, (msg) =>
33+
{
34+
// Verify we aren't setting it twice
35+
messageReceivedSignal.TrySetResult(msg);
36+
}))
37+
{
38+
// 2. Start the Server
39+
server.Start();
40+
41+
// 3. Send the message with a "Retry Policy"
42+
// We try to connect 5 times. If the server is slow to start, this handles it.
43+
bool connected = await TrySendTcpMessageAsync(TestPort, "Hello World!");
44+
Assert.IsTrue(connected, "Could not connect to the server. It might not have started in time.");
45+
46+
// 4. Wait for the result with a TIMEOUT
47+
// If the signal isn't set in 2 seconds, we force a failure.
48+
// This prevents the "Endless Run".
49+
var completedTask = await Task.WhenAny(messageReceivedSignal.Task, Task.Delay(2000));
50+
51+
if (completedTask != messageReceivedSignal.Task)
52+
{
53+
Assert.Fail("Test Timed Out: Server accepted connection but never processed the message.");
54+
}
55+
56+
Assert.AreEqual("Hello World!", messageReceivedSignal.Task.Result);
57+
}
58+
}
59+
60+
/// <summary>
61+
/// Tries the send TCP message asynchronous.
62+
/// </summary>
63+
/// <param name="port">The port.</param>
64+
/// <param name="message">The message.</param>
65+
/// <returns>Message Status.</returns>
66+
private async Task<bool> TrySendTcpMessageAsync(int port, string message)
67+
{
68+
for (int i = 0; i < 5; i++)
69+
{
70+
try
71+
{
72+
using (var client = new TcpClient())
73+
{
74+
// Try to connect
75+
await client.ConnectAsync("127.0.0.1", port);
76+
77+
using (var stream = client.GetStream())
78+
{
79+
// Important: Add NewLine (\n) so ReadLineAsync fires!
80+
byte[] data = Encoding.UTF8.GetBytes(message + Environment.NewLine);
81+
await stream.WriteAsync(data, 0, data.Length);
82+
await stream.FlushAsync();
83+
}
84+
}
85+
return true; // Success!
86+
}
87+
catch (SocketException)
88+
{
89+
// Server not ready yet? Wait 100ms and try again.
90+
await Task.Delay(100);
91+
}
92+
}
93+
return false; // Failed after 5 attempts
94+
}
95+
96+
/// <summary>
97+
/// Interfaces the constructor should use processor class.
98+
/// </summary>
99+
[TestMethod]
100+
public async Task InterfaceConstructor_ShouldUseProcessorClass()
101+
{
102+
// Arrange
103+
var fakeProcessor = new FakeLogProcessor();
104+
var port = TestPort + 1;
105+
106+
// Create server using the INTERFACE constructor
107+
using (var server = new SimpleLogServer(port, fakeProcessor))
108+
{
109+
// Act
110+
server.Start();
111+
112+
// FIX: Use 'TrySendTcpMessageAsync' (Retry Logic) instead of 'SendTcpMessageAsync'
113+
// We also check the boolean result to ensure connection happened.
114+
bool connected = await TrySendTcpMessageAsync(port, "Hello from Class!");
115+
Assert.IsTrue(connected, "Could not connect to the server (Timeout).");
116+
117+
// Wait for the processor to get the message (Polling with timeout)
118+
string received = null;
119+
for (var i = 0; i < 20; i++) // Try for 2 seconds (20 * 100ms)
120+
{
121+
if (fakeProcessor.LastMessage != null)
122+
{
123+
received = fakeProcessor.LastMessage;
124+
break;
125+
}
126+
await Task.Delay(100);
127+
}
128+
129+
// Assert
130+
Assert.IsNotNull(received, "Message was never received by the processor.");
131+
Assert.AreEqual("Hello from Class!", received);
132+
}
133+
}
134+
135+
/// <summary>
136+
/// Servers the should disconnect slow clients.
137+
/// </summary>
138+
[TestMethod]
139+
public async Task Server_ShouldDisconnect_SlowClients()
140+
{
141+
// Arrange
142+
int port = 55150;
143+
using (var server = new SimpleLogServer(port, msg => { }))
144+
{
145+
server.Start();
146+
await Task.Delay(100);
147+
148+
using (var client = new TcpClient())
149+
{
150+
await client.ConnectAsync("127.0.0.1", port);
151+
using (var stream = client.GetStream())
152+
{
153+
// Act: Send data WITHOUT a newline
154+
byte[] data = Encoding.UTF8.GetBytes("I am a bad client...");
155+
await stream.WriteAsync(data, 0, data.Length);
156+
157+
// Don't send \n. Just wait.
158+
// The server should cut us off after 5 seconds.
159+
160+
// Assert: Try to read from the stream.
161+
// If the server disconnects us, ReadAsync returns 0.
162+
byte[] buffer = new byte[10];
163+
int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length);
164+
165+
// If bytesRead is 0, it means the server closed the connection.
166+
Assert.AreEqual(0, bytesRead, "Server should have closed the connection due to timeout.");
167+
}
168+
}
169+
}
170+
}
171+
172+
/// <inheritdoc />
173+
/// <summary>
174+
/// Helper: A Fake Processor for testing
175+
/// </summary>
176+
/// <seealso cref="Communication.Interfaces.ILogProcessor" />
177+
private class FakeLogProcessor : ILogProcessor
178+
{
179+
/// <summary>
180+
/// Gets or sets the last message.
181+
/// </summary>
182+
/// <value>
183+
/// The last message.
184+
/// </value>
185+
public string LastMessage { get; private set; }
186+
187+
/// <summary>
188+
/// Processes the received message (e.g., save to DB, log to file).
189+
/// </summary>
190+
/// <param name="message">The message received from the client.</param>
191+
/// <returns>Message Status.</returns>
192+
public Task ProcessMessageAsync(string message)
193+
{
194+
LastMessage = message;
195+
return Task.CompletedTask;
196+
}
197+
}
198+
}
199+
}

Imaging/Gifs/ImageGifHandler.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
// ReSharper disable MemberCanBeInternal
1313
// ReSharper disable UnusedAutoPropertyAccessor.Global
1414

15-
using System;
1615
using System.Collections.Generic;
1716
using System.Diagnostics;
1817
using System.Drawing;

Imaging/Helpers/ImageStream.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
using System.Runtime.InteropServices;
2323
using ExtendedSystemObjects;
2424
using Imaging.Enums;
25-
using Mathematics;
2625
using Mathematics.Constants;
2726

2827
namespace Imaging.Helpers

Imaging/Helpers/ImageStreamHsv.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99
using System;
1010
using System.Drawing;
11-
using static OpenTK.Graphics.OpenGL.GL;
1211

1312
namespace Imaging.Helpers
1413
{

Imaging/Helpers/TextureStream.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
using System.Drawing;
1212
using System.Drawing.Drawing2D;
1313
using System.Runtime.CompilerServices;
14-
using static OpenTK.Graphics.OpenGL.GL;
1514

1615
// ReSharper disable UnusedMember.Local
1716

0 commit comments

Comments
 (0)