using System.IO; using System.IO.Ports; using System.Reflection; using System.Threading; using System.Threading.Tasks; using AMWD.Protocols.Modbus.Common.Contracts; using AMWD.Protocols.Modbus.Serial; using AMWD.Protocols.Modbus.Serial.Enums; using AMWD.Protocols.Modbus.Serial.Utils; namespace AMWD.Protocols.Modbus.Tests.Serial { [TestClass] public class ModbusSerialConnectionTest { public TestContext TestContext { get; set; } private Mock _serialPortMock; private bool _alwaysOpen; private Queue _isOpenQueue; private readonly int _serialPortReadTimeout = 1000; private readonly int _serialPortWriteTimeout = 1000; private List _serialLineRequestCallbacks; private Queue _serialLineResponseQueue; [TestInitialize] public void Initialize() { _alwaysOpen = true; _isOpenQueue = new Queue(); _serialLineRequestCallbacks = []; _serialLineResponseQueue = new Queue(); } [TestMethod] public void ShouldGetAndSetPropertiesOfBaseClient() { // Arrange var connection = GetSerialConnection(); // Act connection.PortName = "SerialPort"; connection.BaudRate = BaudRate.Baud2400; connection.DataBits = 5; connection.Handshake = Handshake.XOnXOff; connection.Parity = Parity.None; connection.ReadTimeout = TimeSpan.FromSeconds(123); connection.RtsEnable = true; connection.StopBits = StopBits.OnePointFive; connection.WriteTimeout = TimeSpan.FromSeconds(456); // Assert - part 1 _serialPortMock.VerifySet(p => p.PortName = "SerialPort", Times.Once); _serialPortMock.VerifySet(p => p.BaudRate = 2400, Times.Once); _serialPortMock.VerifySet(p => p.DataBits = 5, Times.Once); _serialPortMock.VerifySet(p => p.Handshake = Handshake.XOnXOff, Times.Once); _serialPortMock.VerifySet(p => p.Parity = Parity.None, Times.Once); _serialPortMock.VerifySet(p => p.ReadTimeout = 123000, Times.Once); _serialPortMock.VerifySet(p => p.RtsEnable = true, Times.Once); _serialPortMock.VerifySet(p => p.StopBits = StopBits.OnePointFive, Times.Once); _serialPortMock.VerifySet(p => p.WriteTimeout = 456000, Times.Once); _serialPortMock.VerifyNoOtherCalls(); // Assert - part 2 Assert.AreEqual("Serial", connection.Name); Assert.IsNull(connection.PortName); Assert.AreEqual(0, (int)connection.BaudRate); Assert.AreEqual(0, connection.DataBits); Assert.AreEqual(0, (int)connection.Handshake); Assert.AreEqual(0, (int)connection.Parity); Assert.AreEqual(1, connection.ReadTimeout.TotalSeconds); Assert.IsFalse(connection.RtsEnable); Assert.AreEqual(0, (int)connection.StopBits); Assert.AreEqual(1, connection.WriteTimeout.TotalSeconds); } [TestMethod] public void ShouldBeAbleToDisposeMultipleTimes() { // Arrange var connection = GetConnection(); // Act connection.Dispose(); connection.Dispose(); } [TestMethod] [DataRow(null)] [DataRow("")] [DataRow(" ")] public void ShouldThrowArgumentNullExceptionOnCreate(string portName) { // Arrange // Act + Assert Assert.ThrowsExactly(() => new ModbusSerialClient(portName)); } [TestMethod] public async Task ShouldThrowDisposedExceptionOnInvokeAsync() { // Arrange var connection = GetConnection(); connection.Dispose(); // Act + Assert await Assert.ThrowsExactlyAsync(() => connection.InvokeAsync(null, null, TestContext.CancellationToken)); } [TestMethod] [DataRow(null)] [DataRow(new byte[0])] public async Task ShouldThrowArgumentNullExceptionForMissingRequestOnInvokeAsync(byte[] request) { // Arrange var connection = GetConnection(); // Act + Assert await Assert.ThrowsExactlyAsync(() => connection.InvokeAsync(request, null, TestContext.CancellationToken)); } [TestMethod] public async Task ShouldThrowArgumentNullExceptionForMissingValidationOnInvokeAsync() { // Arrange byte[] request = new byte[1]; var connection = GetConnection(); // Act + Assert await Assert.ThrowsExactlyAsync(() => connection.InvokeAsync(request, null, TestContext.CancellationToken)); } [TestMethod] public async Task ShouldInvokeAsync() { // Arrange byte[] request = [1, 2, 3]; byte[] expectedResponse = [9, 8, 7]; var validation = new Func, bool>(_ => true); _serialLineResponseQueue.Enqueue(expectedResponse); var connection = GetConnection(); // Act var response = await connection.InvokeAsync(request, validation, TestContext.CancellationToken); // Assert Assert.IsNotNull(response); CollectionAssert.AreEqual(expectedResponse, response.ToArray()); CollectionAssert.AreEqual(request, _serialLineRequestCallbacks.First()); _serialPortMock.Verify(c => c.IsOpen, Times.Once); _serialPortMock.Verify(ns => ns.WriteAsync(It.IsAny(), It.IsAny()), Times.Once); _serialPortMock.Verify(ns => ns.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); _serialPortMock.VerifyNoOtherCalls(); } [TestMethod] public async Task ShouldOpenAndCloseOnInvokeAsyncOnLinuxNotModifyingDriver() { // Arrange _alwaysOpen = false; _isOpenQueue.Enqueue(false); _isOpenQueue.Enqueue(true); _isOpenQueue.Enqueue(true); byte[] request = [1, 2, 3]; byte[] expectedResponse = [9, 8, 7]; var validation = new Func, bool>(_ => true); _serialLineResponseQueue.Enqueue(expectedResponse); var connection = GetSerialConnection(); connection.GetType().GetField("_isLinux", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(connection, true); connection.IdleTimeout = TimeSpan.FromMilliseconds(200); connection.DriverEnabledRS485 = false; // Act var response = await connection.InvokeAsync(request, validation, TestContext.CancellationToken); await Task.Delay(500, TestContext.CancellationToken); // Assert Assert.IsNotNull(response); CollectionAssert.AreEqual(expectedResponse, response.ToArray()); CollectionAssert.AreEqual(request, _serialLineRequestCallbacks.First()); _serialPortMock.VerifyGet(c => c.ReadTimeout, Times.Once); _serialPortMock.Verify(c => c.IsOpen, Times.Exactly(3)); _serialPortMock.Verify(c => c.Close(), Times.Exactly(2)); _serialPortMock.Verify(c => c.ResetRS485DriverStateFlags(), Times.Exactly(2)); _serialPortMock.Verify(c => c.Open(), Times.Once); _serialPortMock.Verify(ns => ns.WriteAsync(It.IsAny(), It.IsAny()), Times.Once); _serialPortMock.Verify(ns => ns.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); _serialPortMock.VerifyNoOtherCalls(); } [TestMethod] public async Task ShouldOpenAndCloseOnInvokeAsyncOnLinuxModifyingDriver() { // Arrange _alwaysOpen = false; _isOpenQueue.Enqueue(false); _isOpenQueue.Enqueue(true); _isOpenQueue.Enqueue(true); byte[] request = [1, 2, 3]; byte[] expectedResponse = [9, 8, 7]; var validation = new Func, bool>(_ => true); _serialLineResponseQueue.Enqueue(expectedResponse); var connection = GetSerialConnection(); connection.GetType().GetField("_isLinux", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(connection, true); connection.IdleTimeout = TimeSpan.FromMilliseconds(200); connection.DriverEnabledRS485 = true; // Act var response = await connection.InvokeAsync(request, validation, TestContext.CancellationToken); await Task.Delay(500, TestContext.CancellationToken); // Assert Assert.IsNotNull(response); CollectionAssert.AreEqual(expectedResponse, response.ToArray()); CollectionAssert.AreEqual(request, _serialLineRequestCallbacks.First()); _serialPortMock.VerifyGet(c => c.ReadTimeout, Times.Once); _serialPortMock.Verify(c => c.IsOpen, Times.Exactly(3)); _serialPortMock.Verify(c => c.Close(), Times.Exactly(2)); _serialPortMock.Verify(c => c.ResetRS485DriverStateFlags(), Times.Exactly(2)); _serialPortMock.Verify(c => c.Open(), Times.Once); _serialPortMock.Verify(c => c.GetRS485DriverStateFlags(), Times.Once); _serialPortMock.Verify(c => c.ChangeRS485DriverStateFlags(It.IsAny()), Times.Once); _serialPortMock.Verify(ns => ns.WriteAsync(It.IsAny(), It.IsAny()), Times.Once); _serialPortMock.Verify(ns => ns.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); _serialPortMock.VerifyNoOtherCalls(); } [TestMethod] public async Task ShouldOpenAndCloseOnInvokeAsyncOnOtherOsNotModifyingDriver() { // Arrange _alwaysOpen = false; _isOpenQueue.Enqueue(false); _isOpenQueue.Enqueue(true); _isOpenQueue.Enqueue(true); byte[] request = [1, 2, 3]; byte[] expectedResponse = [9, 8, 7]; var validation = new Func, bool>(_ => true); _serialLineResponseQueue.Enqueue(expectedResponse); var connection = GetSerialConnection(); connection.GetType().GetField("_isLinux", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(connection, false); connection.IdleTimeout = TimeSpan.FromMilliseconds(200); connection.DriverEnabledRS485 = false; // Act var response = await connection.InvokeAsync(request, validation, TestContext.CancellationToken); await Task.Delay(500, TestContext.CancellationToken); // Assert Assert.IsNotNull(response); CollectionAssert.AreEqual(expectedResponse, response.ToArray()); CollectionAssert.AreEqual(request, _serialLineRequestCallbacks.First()); _serialPortMock.VerifyGet(c => c.ReadTimeout, Times.Once); _serialPortMock.Verify(c => c.IsOpen, Times.Exactly(3)); _serialPortMock.Verify(c => c.Close(), Times.Exactly(2)); _serialPortMock.Verify(c => c.ResetRS485DriverStateFlags(), Times.Exactly(2)); _serialPortMock.Verify(c => c.Open(), Times.Once); _serialPortMock.Verify(ns => ns.WriteAsync(It.IsAny(), It.IsAny()), Times.Once); _serialPortMock.Verify(ns => ns.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); _serialPortMock.VerifyNoOtherCalls(); } [TestMethod] public async Task ShouldOpenAndCloseOnInvokeAsyncOnOtherOsModifyingDriver() { // Arrange _alwaysOpen = false; _isOpenQueue.Enqueue(false); _isOpenQueue.Enqueue(true); _isOpenQueue.Enqueue(true); byte[] request = [1, 2, 3]; byte[] expectedResponse = [9, 8, 7]; var validation = new Func, bool>(_ => true); _serialLineResponseQueue.Enqueue(expectedResponse); var connection = GetSerialConnection(); connection.GetType().GetField("_isLinux", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(connection, false); connection.IdleTimeout = TimeSpan.FromMilliseconds(200); connection.DriverEnabledRS485 = true; // Act var response = await connection.InvokeAsync(request, validation, TestContext.CancellationToken); await Task.Delay(500, TestContext.CancellationToken); // Assert Assert.IsNotNull(response); CollectionAssert.AreEqual(expectedResponse, response.ToArray()); CollectionAssert.AreEqual(request, _serialLineRequestCallbacks.First()); _serialPortMock.VerifyGet(c => c.ReadTimeout, Times.Once); _serialPortMock.Verify(c => c.IsOpen, Times.Exactly(3)); _serialPortMock.Verify(c => c.Close(), Times.Exactly(2)); _serialPortMock.Verify(c => c.ResetRS485DriverStateFlags(), Times.Exactly(2)); _serialPortMock.Verify(c => c.Open(), Times.Once); _serialPortMock.Verify(ns => ns.WriteAsync(It.IsAny(), It.IsAny()), Times.Once); _serialPortMock.Verify(ns => ns.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); _serialPortMock.VerifyNoOtherCalls(); } [TestMethod] public async Task ShouldThrowEndOfStreamExceptionOnInvokeAsync() { // Arrange byte[] request = [1, 2, 3]; var validation = new Func, bool>(_ => true); var connection = GetConnection(); // Act + Assert await Assert.ThrowsExactlyAsync(() => connection.InvokeAsync(request, validation, TestContext.CancellationToken)); } [TestMethod] public async Task ShouldSkipCloseOnTimeoutOnInvokeAsync() { // Arrange _alwaysOpen = false; _isOpenQueue.Enqueue(false); _isOpenQueue.Enqueue(true); _isOpenQueue.Enqueue(false); byte[] request = [1, 2, 3]; byte[] expectedResponse = [9, 8, 7]; var validation = new Func, bool>(_ => true); _serialLineResponseQueue.Enqueue(expectedResponse); var connection = GetConnection(); connection.IdleTimeout = TimeSpan.FromMilliseconds(200); // Act var response = await connection.InvokeAsync(request, validation, TestContext.CancellationToken); await Task.Delay(500, TestContext.CancellationToken); // Assert Assert.IsNotNull(response); CollectionAssert.AreEqual(expectedResponse, response.ToArray()); CollectionAssert.AreEqual(request, _serialLineRequestCallbacks.First()); _serialPortMock.VerifyGet(c => c.ReadTimeout, Times.Once); _serialPortMock.Verify(c => c.IsOpen, Times.Exactly(3)); _serialPortMock.Verify(c => c.Close(), Times.Once); _serialPortMock.Verify(c => c.ResetRS485DriverStateFlags(), Times.Once); _serialPortMock.Verify(c => c.Open(), Times.Once); _serialPortMock.Verify(ns => ns.WriteAsync(It.IsAny(), It.IsAny()), Times.Once); _serialPortMock.Verify(ns => ns.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); _serialPortMock.VerifyNoOtherCalls(); } [TestMethod] public async Task ShouldRetryToConnectOnInvokeAsync() { // Arrange _alwaysOpen = false; _isOpenQueue.Enqueue(false); _isOpenQueue.Enqueue(false); _isOpenQueue.Enqueue(true); byte[] request = [1, 2, 3]; byte[] expectedResponse = [9, 8, 7]; var validation = new Func, bool>(_ => true); _serialLineResponseQueue.Enqueue(expectedResponse); var connection = GetConnection(); // Act var response = await connection.InvokeAsync(request, validation, TestContext.CancellationToken); // Assert Assert.IsNotNull(response); CollectionAssert.AreEqual(expectedResponse, response.ToArray()); CollectionAssert.AreEqual(request, _serialLineRequestCallbacks.First()); _serialPortMock.VerifyGet(c => c.ReadTimeout, Times.Exactly(2)); _serialPortMock.Verify(c => c.IsOpen, Times.Exactly(3)); _serialPortMock.Verify(c => c.Close(), Times.Exactly(2)); _serialPortMock.Verify(c => c.ResetRS485DriverStateFlags(), Times.Exactly(2)); _serialPortMock.Verify(c => c.Open(), Times.Exactly(2)); _serialPortMock.Verify(ns => ns.WriteAsync(It.IsAny(), It.IsAny()), Times.Once); _serialPortMock.Verify(ns => ns.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); _serialPortMock.VerifyNoOtherCalls(); } [TestMethod] public async Task ShouldThrowTaskCancelledExceptionForDisposeOnInvokeAsync() { // Arrange byte[] request = [1, 2, 3]; var validation = new Func, bool>(_ => true); var connection = GetConnection(); _serialPortMock .Setup(ns => ns.WriteAsync(It.IsAny(), It.IsAny())) .Returns((_, ct) => Task.Delay(100, ct)); // Act + Assert await Assert.ThrowsExactlyAsync(async () => { var task = connection.InvokeAsync(request, validation, TestContext.CancellationToken); connection.Dispose(); await task; }); } [TestMethod] public async Task ShouldThrowTaskCancelledExceptionForCancelOnInvokeAsync() { // Arrange byte[] request = [1, 2, 3]; var validation = new Func, bool>(_ => true); using var cts = new CancellationTokenSource(); var connection = GetConnection(); _serialPortMock .Setup(ns => ns.WriteAsync(It.IsAny(), It.IsAny())) .Returns((_, ct) => Task.Delay(100, ct)); // Act + Assert await Assert.ThrowsExactlyAsync(async () => { var task = connection.InvokeAsync(request, validation, cts.Token); cts.Cancel(); await task; }); } [TestMethod] public async Task ShouldRemoveRequestFromQueueOnInvokeAsync() { // Arrange byte[] request = [1, 2, 3]; byte[] expectedResponse = [9, 8, 7]; var validation = new Func, bool>(_ => true); _serialLineResponseQueue.Enqueue(expectedResponse); using var cts = new CancellationTokenSource(); var connection = GetConnection(); _serialPortMock .Setup(ns => ns.WriteAsync(It.IsAny(), It.IsAny())) .Callback((req, _) => _serialLineRequestCallbacks.Add([.. req])) .Returns((_, ct) => Task.Delay(100, ct)); // Act var taskToComplete = connection.InvokeAsync(request, validation, TestContext.CancellationToken); var taskToCancel = connection.InvokeAsync(request, validation, cts.Token); cts.Cancel(); var response = await taskToComplete; // Assert - Part 1 await Assert.ThrowsExactlyAsync(async () => await taskToCancel); // Assert - Part 2 Assert.HasCount(1, _serialLineRequestCallbacks); CollectionAssert.AreEqual(request, _serialLineRequestCallbacks.First()); CollectionAssert.AreEqual(expectedResponse, response.ToArray()); _serialPortMock.Verify(c => c.IsOpen, Times.Once); _serialPortMock.Verify(ns => ns.WriteAsync(It.IsAny(), It.IsAny()), Times.Once); _serialPortMock.Verify(ns => ns.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); _serialPortMock.VerifyNoOtherCalls(); } [TestMethod] public async Task ShouldRemoveRequestFromQueueOnDispose() { // Arrange byte[] request = [1, 2, 3]; var validation = new Func, bool>(_ => true); var connection = GetConnection(); _serialPortMock .Setup(ns => ns.WriteAsync(It.IsAny(), It.IsAny())) .Callback((req, _) => _serialLineRequestCallbacks.Add([.. req])) .Returns((_, ct) => Task.Delay(100, ct)); // Act var taskToCancel = connection.InvokeAsync(request, validation, TestContext.CancellationToken); var taskToDequeue = connection.InvokeAsync(request, validation, TestContext.CancellationToken); connection.Dispose(); // Assert await Assert.ThrowsExactlyAsync(async () => await taskToCancel); await Assert.ThrowsExactlyAsync(async () => await taskToDequeue); Assert.HasCount(1, _serialLineRequestCallbacks); CollectionAssert.AreEqual(request, _serialLineRequestCallbacks.First()); _serialPortMock.Verify(c => c.IsOpen, Times.Once); _serialPortMock.Verify(c => c.Dispose(), Times.Once); _serialPortMock.Verify(ns => ns.WriteAsync(It.IsAny(), It.IsAny()), Times.Once); _serialPortMock.VerifyNoOtherCalls(); } private IModbusConnection GetConnection() => GetSerialConnection(); private ModbusSerialConnection GetSerialConnection() { _serialPortMock = new Mock(); _serialPortMock.Setup(p => p.IsOpen).Returns(() => _alwaysOpen || _isOpenQueue.Dequeue()); _serialPortMock.Setup(p => p.ReadTimeout).Returns(() => _serialPortReadTimeout); _serialPortMock.Setup(p => p.WriteTimeout).Returns(() => _serialPortWriteTimeout); _serialPortMock .Setup(p => p.WriteAsync(It.IsAny(), It.IsAny())) .Callback((req, _) => _serialLineRequestCallbacks.Add(req)) .Returns(Task.CompletedTask); _serialPortMock .Setup(p => p.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns((buffer, offset, count, _) => { if (_serialLineResponseQueue.TryDequeue(out byte[] bytes)) { int len = bytes.Length < count ? bytes.Length : count; Array.Copy(bytes, 0, buffer, offset, len); return Task.FromResult(len); } return Task.FromResult(0); }); var connection = new ModbusSerialConnection("some-port"); // Replace real connection with mock var connectionField = connection.GetType().GetField("_serialPort", BindingFlags.NonPublic | BindingFlags.Instance); (connectionField.GetValue(connection) as SerialPortWrapper)?.Dispose(); connectionField.SetValue(connection, _serialPortMock.Object); return connection; } } }