From 0d87dbf3c6e5bceb5f873a71b12b948ef5c922e0 Mon Sep 17 00:00:00 2001 From: Serge Camille Date: Mon, 7 Jun 2021 14:49:06 +0200 Subject: [PATCH 1/2] Add unit test to ensure a connection has been properly closed If cancelled during data transmission, #349 adds code to ensure that the connection automatically gets cancelled. Add inerted test to ensure that cancellation before sending data does not result in a cancellation. --- S7.Net.UnitTest/ConnectionCloseTest.cs | 183 +++++++++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 S7.Net.UnitTest/ConnectionCloseTest.cs diff --git a/S7.Net.UnitTest/ConnectionCloseTest.cs b/S7.Net.UnitTest/ConnectionCloseTest.cs new file mode 100644 index 0000000..f91c88e --- /dev/null +++ b/S7.Net.UnitTest/ConnectionCloseTest.cs @@ -0,0 +1,183 @@ +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 + cancellationSource.CancelAfter(TimeSpan.FromMilliseconds(5)); + 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 + cancellationSource.CancelAfter(TimeSpan.FromMilliseconds(5)); + 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."); + } + } +} From 74fecad48df11b1c9d44dca455b1fd6fd4dc842c Mon Sep 17 00:00:00 2001 From: Serge Camille Date: Tue, 8 Jun 2021 22:47:12 +0200 Subject: [PATCH 2/2] Remove cancel timeouts. --- S7.Net.UnitTest/ConnectionCloseTest.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/S7.Net.UnitTest/ConnectionCloseTest.cs b/S7.Net.UnitTest/ConnectionCloseTest.cs index f91c88e..87c9ea9 100644 --- a/S7.Net.UnitTest/ConnectionCloseTest.cs +++ b/S7.Net.UnitTest/ConnectionCloseTest.cs @@ -79,7 +79,6 @@ namespace S7.Net.UnitTest var requestData = new byte[100]; // empty data, it does not matter what is in there // Set up access to private method and field - cancellationSource.CancelAfter(TimeSpan.FromMilliseconds(5)); var dynMethod = plc.GetType().GetMethod("NoLockRequestTpduAsync", BindingFlags.NonPublic | BindingFlags.Instance); if (dynMethod == null) @@ -135,7 +134,6 @@ namespace S7.Net.UnitTest var requestData = new byte[100]; // empty data, it does not matter what is in there // Set up access to private method and field - cancellationSource.CancelAfter(TimeSpan.FromMilliseconds(5)); var dynMethod = plc.GetType().GetMethod("NoLockRequestTpduAsync", BindingFlags.NonPublic | BindingFlags.Instance); if (dynMethod == null)