Skip to content

Commit 3ca5a29

Browse files
tylerkronclaude
andauthored
Phase 2: Complete Message System Migration with Desktop Integration (#33)
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 724fdce commit 3ca5a29

28 files changed

+4102
-39
lines changed

PHASE2_STEP1_EXAMPLE.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Phase 2 Steps 1-2 Complete: Message Producer with Threading
2+
3+
## What Was Added
4+
5+
**IMessageProducer<T>** interface with lifecycle management
6+
**MessageProducer<T>** with background threading (Step 2)
7+
**DaqifiDevice** updated to optionally use message producer
8+
**Comprehensive tests** including threading validation
9+
**Backward compatibility** maintained
10+
**Cross-platform** implementation (no Windows dependencies)
11+
12+
## Usage Example
13+
14+
### Before (Desktop only):
15+
```csharp
16+
// Desktop had to manage its own MessageProducer
17+
var stream = new TcpClient().GetStream();
18+
var producer = new Daqifi.Desktop.IO.Messages.Producers.MessageProducer(stream);
19+
producer.Start();
20+
producer.Send(Daqifi.Core.Communication.Producers.ScpiMessageProducer.GetDeviceInfo);
21+
```
22+
23+
### After (Using Core):
24+
```csharp
25+
// Core now provides the message producer
26+
using var stream = new TcpClient().GetStream();
27+
using var device = new DaqifiDevice("My Device", stream, IPAddress.Parse("192.168.1.100"));
28+
29+
device.Connect(); // Automatically starts message producer
30+
device.Send(ScpiMessageProducer.GetDeviceInfo); // Uses Core's thread-safe producer
31+
// device.Disconnect(); // Automatically stops message producer safely
32+
```
33+
34+
## Testing the Implementation
35+
36+
All tests pass (59/59) including:
37+
- Message producer lifecycle management
38+
- Background thread processing and lifecycle
39+
- Thread-safe message queuing with asynchronous processing
40+
- Device integration with message producer
41+
- Error handling and validation
42+
- Backward compatibility scenarios
43+
44+
## Current State vs Desktop
45+
46+
**Desktop MessageProducer**: Windows-specific, string-only, Thread + ConcurrentQueue
47+
**Core MessageProducer<T>**: Cross-platform, generic, Thread + ConcurrentQueue
48+
**Functionality**: ✅ **Identical** - Core now matches desktop's threading behavior
49+
50+
## Next Steps
51+
52+
**Step 3**: Add transport abstraction (TCP/UDP/Serial interfaces)
53+
**Step 4**: Device discovery framework
54+
55+
## Desktop Integration Path
56+
57+
The desktop can now:
58+
1. **Gradually adopt** Core's message producer by using the new DaqifiDevice constructor
59+
2. **Keep existing code working** - no breaking changes
60+
3. **Test side-by-side** - old and new implementations can coexist
61+
4. **Migrate incrementally** - device by device, connection by connection
62+
63+
This completes Steps 1-2 of Phase 2 migration! 🎉
64+
65+
**Core MessageProducer is now functionally equivalent to Desktop's implementation** but cross-platform and generic.
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
using Daqifi.Core.Communication.Consumers;
2+
using System.Text;
3+
4+
namespace Daqifi.Core.Tests.Communication.Consumers;
5+
6+
public class LineBasedMessageParserTests
7+
{
8+
[Fact]
9+
public void LineBasedMessageParser_ParseMessages_WithSingleLine_ShouldReturnOneMessage()
10+
{
11+
// Arrange
12+
var parser = new LineBasedMessageParser();
13+
var data = Encoding.UTF8.GetBytes("Hello World\r\n");
14+
15+
// Act
16+
var messages = parser.ParseMessages(data, out var consumedBytes);
17+
18+
// Assert
19+
Assert.Single(messages);
20+
Assert.Equal("Hello World", messages.First().Data);
21+
Assert.Equal(data.Length, consumedBytes);
22+
}
23+
24+
[Fact]
25+
public void LineBasedMessageParser_ParseMessages_WithMultipleLines_ShouldReturnMultipleMessages()
26+
{
27+
// Arrange
28+
var parser = new LineBasedMessageParser();
29+
var data = Encoding.UTF8.GetBytes("Line 1\r\nLine 2\r\nLine 3\r\n");
30+
31+
// Act
32+
var messages = parser.ParseMessages(data, out var consumedBytes);
33+
34+
// Assert
35+
Assert.Equal(3, messages.Count());
36+
Assert.Equal("Line 1", messages.ElementAt(0).Data);
37+
Assert.Equal("Line 2", messages.ElementAt(1).Data);
38+
Assert.Equal("Line 3", messages.ElementAt(2).Data);
39+
Assert.Equal(data.Length, consumedBytes);
40+
}
41+
42+
[Fact]
43+
public void LineBasedMessageParser_ParseMessages_WithIncompleteMessage_ShouldNotConsumeIncomplete()
44+
{
45+
// Arrange
46+
var parser = new LineBasedMessageParser();
47+
var data = Encoding.UTF8.GetBytes("Complete Line\r\nIncomplete");
48+
49+
// Act
50+
var messages = parser.ParseMessages(data, out var consumedBytes);
51+
52+
// Assert
53+
Assert.Single(messages);
54+
Assert.Equal("Complete Line", messages.First().Data);
55+
Assert.Equal(15, consumedBytes); // "Complete Line\r\n" length
56+
}
57+
58+
[Fact]
59+
public void LineBasedMessageParser_ParseMessages_WithEmptyLines_ShouldIgnoreEmpty()
60+
{
61+
// Arrange
62+
var parser = new LineBasedMessageParser();
63+
var data = Encoding.UTF8.GetBytes("Line 1\r\n\r\nLine 2\r\n");
64+
65+
// Act
66+
var messages = parser.ParseMessages(data, out var consumedBytes);
67+
68+
// Assert
69+
Assert.Equal(2, messages.Count());
70+
Assert.Equal("Line 1", messages.ElementAt(0).Data);
71+
Assert.Equal("Line 2", messages.ElementAt(1).Data);
72+
}
73+
74+
[Fact]
75+
public void LineBasedMessageParser_ParseMessages_WithCustomLineEnding_ShouldWork()
76+
{
77+
// Arrange
78+
var parser = new LineBasedMessageParser("\n"); // LF only
79+
var data = Encoding.UTF8.GetBytes("Line 1\nLine 2\n");
80+
81+
// Act
82+
var messages = parser.ParseMessages(data, out var consumedBytes);
83+
84+
// Assert
85+
Assert.Equal(2, messages.Count());
86+
Assert.Equal("Line 1", messages.ElementAt(0).Data);
87+
Assert.Equal("Line 2", messages.ElementAt(1).Data);
88+
}
89+
90+
[Fact]
91+
public void LineBasedMessageParser_ParseMessages_WithNoData_ShouldReturnEmpty()
92+
{
93+
// Arrange
94+
var parser = new LineBasedMessageParser();
95+
var data = new byte[0];
96+
97+
// Act
98+
var messages = parser.ParseMessages(data, out var consumedBytes);
99+
100+
// Assert
101+
Assert.Empty(messages);
102+
Assert.Equal(0, consumedBytes);
103+
}
104+
}
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
using Daqifi.Core.Communication.Consumers;
2+
using System.Text;
3+
4+
namespace Daqifi.Core.Tests.Communication.Consumers;
5+
6+
public class StreamMessageConsumerTests
7+
{
8+
[Fact]
9+
public void StreamMessageConsumer_Constructor_ShouldInitializeCorrectly()
10+
{
11+
// Arrange
12+
using var stream = new MemoryStream();
13+
var parser = new LineBasedMessageParser();
14+
15+
// Act
16+
using var consumer = new StreamMessageConsumer<string>(stream, parser);
17+
18+
// Assert
19+
Assert.False(consumer.IsRunning);
20+
Assert.Equal(0, consumer.QueuedMessageCount);
21+
}
22+
23+
[Fact]
24+
public void StreamMessageConsumer_Start_ShouldSetRunningState()
25+
{
26+
// Arrange
27+
using var stream = new MemoryStream();
28+
var parser = new LineBasedMessageParser();
29+
using var consumer = new StreamMessageConsumer<string>(stream, parser);
30+
31+
// Act
32+
consumer.Start();
33+
34+
// Assert
35+
Assert.True(consumer.IsRunning);
36+
37+
consumer.Stop();
38+
}
39+
40+
[Fact]
41+
public void StreamMessageConsumer_Stop_ShouldClearRunningState()
42+
{
43+
// Arrange
44+
using var stream = new MemoryStream();
45+
var parser = new LineBasedMessageParser();
46+
using var consumer = new StreamMessageConsumer<string>(stream, parser);
47+
consumer.Start();
48+
49+
// Act
50+
consumer.Stop();
51+
52+
// Assert
53+
Assert.False(consumer.IsRunning);
54+
}
55+
56+
[Fact]
57+
public void StreamMessageConsumer_MessageReceived_ShouldFireForValidMessages()
58+
{
59+
// Arrange
60+
var testData = Encoding.UTF8.GetBytes("Test Message\r\n");
61+
using var stream = new MemoryStream(testData);
62+
var parser = new LineBasedMessageParser();
63+
using var consumer = new StreamMessageConsumer<string>(stream, parser);
64+
65+
string? receivedMessage = null;
66+
consumer.MessageReceived += (sender, args) => receivedMessage = args.Message.Data;
67+
68+
// Act
69+
consumer.Start();
70+
Thread.Sleep(200); // Give time for processing
71+
consumer.Stop();
72+
73+
// Assert
74+
Assert.Equal("Test Message", receivedMessage);
75+
}
76+
77+
[Fact]
78+
public void StreamMessageConsumer_MultipleMessages_ShouldFireMultipleEvents()
79+
{
80+
// Arrange
81+
var testData = Encoding.UTF8.GetBytes("Message 1\r\nMessage 2\r\nMessage 3\r\n");
82+
using var stream = new MemoryStream(testData);
83+
var parser = new LineBasedMessageParser();
84+
using var consumer = new StreamMessageConsumer<string>(stream, parser);
85+
86+
var receivedMessages = new List<string>();
87+
consumer.MessageReceived += (sender, args) => receivedMessages.Add(args.Message.Data);
88+
89+
// Act
90+
consumer.Start();
91+
Thread.Sleep(300); // Give time for processing
92+
consumer.Stop();
93+
94+
// Assert
95+
Assert.Equal(3, receivedMessages.Count);
96+
Assert.Contains("Message 1", receivedMessages);
97+
Assert.Contains("Message 2", receivedMessages);
98+
Assert.Contains("Message 3", receivedMessages);
99+
}
100+
101+
[Fact]
102+
public void StreamMessageConsumer_ErrorHandling_ShouldFireErrorEvent()
103+
{
104+
// Arrange - Create a stream that will throw when read
105+
var errorStream = new ErrorThrowingStream();
106+
var parser = new LineBasedMessageParser();
107+
using var consumer = new StreamMessageConsumer<string>(errorStream, parser);
108+
109+
Exception? capturedError = null;
110+
var errorReceived = false;
111+
consumer.ErrorOccurred += (sender, args) =>
112+
{
113+
capturedError = args.Error;
114+
errorReceived = true;
115+
};
116+
117+
// Act
118+
consumer.Start();
119+
120+
// Wait for error with timeout
121+
var timeout = DateTime.UtcNow.AddMilliseconds(500);
122+
while (!errorReceived && DateTime.UtcNow < timeout)
123+
{
124+
Thread.Sleep(10);
125+
}
126+
127+
consumer.Stop();
128+
129+
// Assert
130+
Assert.True(errorReceived, "Error event should have been fired");
131+
Assert.NotNull(capturedError);
132+
Assert.IsType<InvalidOperationException>(capturedError);
133+
}
134+
135+
[Fact]
136+
public void StreamMessageConsumer_StopSafely_ShouldReturnTrue()
137+
{
138+
// Arrange
139+
using var stream = new MemoryStream();
140+
var parser = new LineBasedMessageParser();
141+
using var consumer = new StreamMessageConsumer<string>(stream, parser);
142+
consumer.Start();
143+
144+
// Act
145+
var result = consumer.StopSafely();
146+
147+
// Assert
148+
Assert.True(result);
149+
Assert.False(consumer.IsRunning);
150+
}
151+
152+
[Fact]
153+
public void StreamMessageConsumer_Dispose_ShouldCleanupResources()
154+
{
155+
// Arrange
156+
using var stream = new MemoryStream();
157+
var parser = new LineBasedMessageParser();
158+
var consumer = new StreamMessageConsumer<string>(stream, parser);
159+
consumer.Start();
160+
161+
// Act
162+
consumer.Dispose();
163+
164+
// Assert
165+
Assert.False(consumer.IsRunning);
166+
Assert.Throws<ObjectDisposedException>(() => consumer.Start());
167+
}
168+
169+
// Helper class for testing error scenarios
170+
private class ErrorThrowingStream : Stream
171+
{
172+
public override bool CanRead => true;
173+
public override bool CanSeek => false;
174+
public override bool CanWrite => false;
175+
public override long Length => throw new NotSupportedException();
176+
public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
177+
178+
public override void Flush() { }
179+
180+
public override int Read(byte[] buffer, int offset, int count)
181+
{
182+
throw new InvalidOperationException("Test error for error handling");
183+
}
184+
185+
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
186+
public override void SetLength(long value) => throw new NotSupportedException();
187+
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
188+
}
189+
}

0 commit comments

Comments
 (0)