using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.IO; using System.Linq; using System.Net.Sockets; using System.Reflection; using System.Threading; using System.Threading.Tasks; namespace S7.Net.UnitTest { /// /// Test stream which only gives 1 byte per read. /// class TestStreamConnectionClose : Stream { private readonly CancellationTokenSource _cancellationTokenSource; public TestStreamConnectionClose(CancellationTokenSource cancellationTokenSource) { _cancellationTokenSource = cancellationTokenSource; } public override bool CanRead => false; public override bool CanSeek => throw new NotImplementedException(); public override bool CanWrite => true; public override long Length => throw new NotImplementedException(); public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } public override void Flush() { throw new NotImplementedException(); } public override int Read(byte[] buffer, int offset, int count) { throw new NotImplementedException(); } public override long Seek(long offset, SeekOrigin origin) { throw new NotImplementedException(); } public override void SetLength(long value) { throw new NotImplementedException(); } public override void Write(byte[] buffer, int offset, int count) { _cancellationTokenSource.Cancel(); } } /// /// These tests are intended to test functions and other stream-related special cases. /// [TestClass] public class ConnectionCloseTest { const short TestServerPort = 31122; const string TestServerIp = "127.0.0.1"; [TestMethod] public async Task Test_CancellationDuringTransmission() { var plc = new Plc(CpuType.S7300, TestServerIp, TestServerPort, 0, 2); // Set up a shared cancellation source so we can let the stream // initiate cancel after some data has been written to it. var cancellationSource = new CancellationTokenSource(); var cancellationToken = cancellationSource.Token; var stream = new TestStreamConnectionClose(cancellationSource); var requestData = new byte[100]; // empty data, it does not matter what is in there // Set up access to private method and field var dynMethod = plc.GetType().GetMethod("NoLockRequestTpduAsync", BindingFlags.NonPublic | BindingFlags.Instance); if (dynMethod == null) { throw new NullReferenceException("Could not find method 'NoLockRequestTpduAsync' on Plc object."); } var tcpClientField = plc.GetType().GetField("tcpClient", BindingFlags.NonPublic | BindingFlags.Instance); if (tcpClientField == null) { throw new NullReferenceException("Could not find field 'tcpClient' on Plc object."); } // Set a value to tcpClient field so we can later ensure that it has been closed. tcpClientField.SetValue(plc, new TcpClient()); var tcpClientValue = tcpClientField.GetValue(plc); Assert.IsNotNull(tcpClientValue); try { var result = (Task) dynMethod.Invoke(plc, new object[] { stream, requestData, cancellationToken }); await result; } catch (OperationCanceledException) { Console.WriteLine("Task was cancelled as expected."); // Ensure that the plc connection was closed since the task was cancelled // after data has been sent through the network. We expect that the tcpClient // object was set to NULL var tcpClientValueAfter = tcpClientField.GetValue(plc); Assert.IsNull(tcpClientValueAfter); return; } catch (Exception e) { Assert.Fail($"Wrong exception type received. Expected {typeof(OperationCanceledException)}, received {e.GetType()}."); } // Ensure test fails if cancellation did not occur. Assert.Fail("Task was not cancelled as expected."); } [TestMethod] public async Task Test_CancellationBeforeTransmission() { var plc = new Plc(CpuType.S7300, TestServerIp, TestServerPort, 0, 2); // Set up a cancellation source var cancellationSource = new CancellationTokenSource(); var cancellationToken = cancellationSource.Token; var stream = new TestStreamConnectionClose(cancellationSource); var requestData = new byte[100]; // empty data, it does not matter what is in there // Set up access to private method and field var dynMethod = plc.GetType().GetMethod("NoLockRequestTpduAsync", BindingFlags.NonPublic | BindingFlags.Instance); if (dynMethod == null) { throw new NullReferenceException("Could not find method 'NoLockRequestTpduAsync' on Plc object."); } var tcpClientField = plc.GetType().GetField("tcpClient", BindingFlags.NonPublic | BindingFlags.Instance); if (tcpClientField == null) { throw new NullReferenceException("Could not find field 'tcpClient' on Plc object."); } // Set a value to tcpClient field so we can later ensure that it has been closed. tcpClientField.SetValue(plc, new TcpClient()); var tcpClientValue = tcpClientField.GetValue(plc); Assert.IsNotNull(tcpClientValue); try { // cancel the task before we start transmitting data cancellationSource.Cancel(); var result = (Task)dynMethod.Invoke(plc, new object[] { stream, requestData, cancellationToken }); await result; } catch (OperationCanceledException) { Console.WriteLine("Task was cancelled as expected."); // Ensure that the plc connection was not closed, since we cancelled the task before // sending data through the network. We expect that the tcpClient // object was NOT set to NULL var tcpClientValueAfter = tcpClientField.GetValue(plc); Assert.IsNotNull(tcpClientValueAfter); return; } catch (Exception e) { Assert.Fail($"Wrong exception type received. Expected {typeof(OperationCanceledException)}, received {e.GetType()}."); } // Ensure test fails if cancellation did not occur. Assert.Fail("Task was not cancelled as expected."); } } }