From c5bcaddd6350c952aedbfc68db10573f5e46b017 Mon Sep 17 00:00:00 2001 From: Rob Hague Date: Sat, 10 Feb 2024 14:14:19 +0100 Subject: [PATCH] Add tests and benchmarks for ShellStream.Read and Expect Most of them are ignored because they fail. --- .../SshClientBenchmark.cs | 35 ++ .../Classes/ShellStreamTest_ReadExpect.cs | 329 ++++++++++++++++++ 2 files changed, 364 insertions(+) create mode 100644 test/Renci.SshNet.Tests/Classes/ShellStreamTest_ReadExpect.cs diff --git a/test/Renci.SshNet.IntegrationBenchmarks/SshClientBenchmark.cs b/test/Renci.SshNet.IntegrationBenchmarks/SshClientBenchmark.cs index 0ce126e0e..32659ba11 100644 --- a/test/Renci.SshNet.IntegrationBenchmarks/SshClientBenchmark.cs +++ b/test/Renci.SshNet.IntegrationBenchmarks/SshClientBenchmark.cs @@ -1,5 +1,6 @@ ο»Ώusing BenchmarkDotNet.Attributes; +using Renci.SshNet.Common; using Renci.SshNet.IntegrationTests.TestsFixtures; namespace Renci.SshNet.IntegrationBenchmarks @@ -8,6 +9,11 @@ namespace Renci.SshNet.IntegrationBenchmarks [SimpleJob] public class SshClientBenchmark : IntegrationBenchmarkBase { + private static readonly Dictionary ShellStreamTerminalModes = new Dictionary + { + { TerminalModes.ECHO, 0 } + }; + private readonly InfrastructureFixture _infrastructureFixture; private SshClient? _sshClient; @@ -65,5 +71,34 @@ public string RunCommand() { return _sshClient!.RunCommand("echo $'test !@#$%^&*()_+{}:,./<>[];\\|'").Result; } + + [Benchmark] + public string ShellStreamReadLine() + { + using (var shellStream = _sshClient!.CreateShellStream("xterm", 80, 24, 800, 600, 1024, ShellStreamTerminalModes)) + { + shellStream.WriteLine("for i in $(seq 500); do echo \"Within cells. Interlinked. $i\"; sleep 0.001; done; echo \"Username:\";"); + + while (true) + { + var line = shellStream.ReadLine(); + + if (line.EndsWith("500", StringComparison.Ordinal)) + { + return line; + } + } + } + } + + [Benchmark] + public string ShellStreamExpect() + { + using (var shellStream = _sshClient!.CreateShellStream("xterm", 80, 24, 800, 600, 1024, ShellStreamTerminalModes)) + { + shellStream.WriteLine("for i in $(seq 500); do echo \"Within cells. Interlinked. $i\"; sleep 0.001; done; echo \"Username:\";"); + return shellStream.Expect("Username:"); + } + } } } diff --git a/test/Renci.SshNet.Tests/Classes/ShellStreamTest_ReadExpect.cs b/test/Renci.SshNet.Tests/Classes/ShellStreamTest_ReadExpect.cs new file mode 100644 index 000000000..8e0387160 --- /dev/null +++ b/test/Renci.SshNet.Tests/Classes/ShellStreamTest_ReadExpect.cs @@ -0,0 +1,329 @@ +ο»Ώusing System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Moq; + +using Renci.SshNet.Channels; +using Renci.SshNet.Common; + +namespace Renci.SshNet.Tests.Classes +{ + [TestClass] + public class ShellStreamTest_ReadExpect + { + private ShellStream _shellStream; + private ChannelSessionStub _channelSessionStub; + + [TestInitialize] + public void Initialize() + { + _channelSessionStub = new ChannelSessionStub(); + + var connectionInfoMock = new Mock(); + + connectionInfoMock.Setup(p => p.Encoding).Returns(Encoding.UTF8); + + var sessionMock = new Mock(); + + sessionMock.Setup(p => p.ConnectionInfo).Returns(connectionInfoMock.Object); + sessionMock.Setup(p => p.CreateChannelSession()).Returns(_channelSessionStub); + + _shellStream = new ShellStream( + sessionMock.Object, + "terminalName", + columns: 80, + rows: 24, + width: 800, + height: 600, + terminalModeValues: null, + bufferSize: 1024); + } + + [TestMethod] + public void Read_String() + { + _channelSessionStub.Receive(Encoding.UTF8.GetBytes("Hello ")); + _channelSessionStub.Receive(Encoding.UTF8.GetBytes("World!")); + + Assert.AreEqual("Hello World!", _shellStream.Read()); + } + + [TestMethod] + public void Read_Bytes() + { + _channelSessionStub.Receive(Encoding.UTF8.GetBytes("Hello ")); + _channelSessionStub.Receive(Encoding.UTF8.GetBytes("World!")); + + byte[] buffer = new byte[12]; + + Assert.AreEqual(7, _shellStream.Read(buffer, 3, 7)); + CollectionAssert.AreEqual(Encoding.UTF8.GetBytes("\0\0\0Hello W\0\0"), buffer); + + Assert.AreEqual(5, _shellStream.Read(buffer, 0, 12)); + CollectionAssert.AreEqual(Encoding.UTF8.GetBytes("orld!llo W\0\0"), buffer); + } + + [DataTestMethod] + [DataRow("\r\n")] + //[DataRow("\r")] These currently fail. + //[DataRow("\n")] + public void ReadLine(string newLine) + { + _channelSessionStub.Receive(Encoding.UTF8.GetBytes("Hello ")); + _channelSessionStub.Receive(Encoding.UTF8.GetBytes("World!")); + + // We specify a nonzero timeout to avoid waiting infinitely. + Assert.IsNull(_shellStream.ReadLine(TimeSpan.FromTicks(1))); + + _channelSessionStub.Receive(Encoding.UTF8.GetBytes(newLine)); + + Assert.AreEqual("Hello World!", _shellStream.ReadLine(TimeSpan.FromTicks(1))); + Assert.IsNull(_shellStream.ReadLine(TimeSpan.FromTicks(1))); + + _channelSessionStub.Receive(Encoding.UTF8.GetBytes("Second line!" + newLine + "Third line!" + newLine)); + + Assert.AreEqual("Second line!", _shellStream.ReadLine(TimeSpan.FromTicks(1))); + Assert.AreEqual("Third line!", _shellStream.ReadLine(TimeSpan.FromTicks(1))); + Assert.IsNull(_shellStream.ReadLine(TimeSpan.FromTicks(1))); + } + + [DataTestMethod] + [DataRow("\r\n")] + [DataRow("\r")] + [DataRow("\n")] + public void Read_MultipleLines(string newLine) + { + _channelSessionStub.Receive(Encoding.UTF8.GetBytes("Hello ")); + _channelSessionStub.Receive(Encoding.UTF8.GetBytes("World!")); + _channelSessionStub.Receive(Encoding.UTF8.GetBytes(newLine)); + _channelSessionStub.Receive(Encoding.UTF8.GetBytes("Second line!" + newLine + "Third line!" + newLine)); + + Assert.AreEqual("Hello World!" + newLine + "Second line!" + newLine + "Third line!" + newLine, _shellStream.Read()); + } + + [TestMethod] + [Ignore] // Currently returns 0 immediately + public void Read_NonEmptyArray_OnlyReturnsZeroAfterClose() + { + Task closeTask = Task.Run(async () => + { + // For the test to have meaning, we should be in + // the call to Read before closing the channel. + // Impose a short delay to make that more likely. + await Task.Delay(50); + + _channelSessionStub.Close(); + }); + + Assert.AreEqual(0, _shellStream.Read(new byte[16], 0, 16)); + Assert.AreEqual(TaskStatus.RanToCompletion, closeTask.Status); + } + + [TestMethod] + [Ignore] // Currently returns 0 immediately + public void Read_EmptyArray_OnlyReturnsZeroWhenDataAvailable() + { + Task receiveTask = Task.Run(async () => + { + // For the test to have meaning, we should be in + // the call to Read before receiving the data. + // Impose a short delay to make that more likely. + await Task.Delay(50); + + _channelSessionStub.Receive(Encoding.UTF8.GetBytes("Hello World!")); + }); + + Assert.AreEqual(0, _shellStream.Read(Array.Empty(), 0, 0)); + Assert.AreEqual(TaskStatus.RanToCompletion, receiveTask.Status); + } + + [TestMethod] + [Ignore] // Currently hangs + public void ReadLine_NoData_ReturnsNullAfterClose() + { + Task closeTask = Task.Run(async () => + { + await Task.Delay(50); + + _channelSessionStub.Close(); + }); + + Assert.IsNull(_shellStream.ReadLine()); + Assert.AreEqual(TaskStatus.RanToCompletion, closeTask.Status); + } + + [TestMethod] + [Ignore] // Fails because it returns the whole buffer i.e. "Hello World!\r\n12345" + // We might actually want to keep that behaviour, but just make the documentation clearer. + // The Expect documentation says: + // "The text available in the shell that contains all the text that ends with expected expression." + // Does that mean + // 1. the returned string ends with the expected expression; or + // 2. the returned string is all the text in the buffer, which is guaranteed to contain the expected expression? + // The current behaviour is closer to 2. I think the documentation implies 1. + // Either way, there are bugs. + public void Expect() + { + _channelSessionStub.Receive(Encoding.UTF8.GetBytes("Hello ")); + _channelSessionStub.Receive(Encoding.UTF8.GetBytes("World!")); + + Assert.IsNull(_shellStream.Expect("123", TimeSpan.FromTicks(1))); + + _channelSessionStub.Receive(Encoding.UTF8.GetBytes("\r\n12345")); + + // Both of these cases fail + // Case 1 above. + Assert.AreEqual("Hello World!\r\n123", _shellStream.Expect("123")); // Fails, returns "Hello World!\r\n12345" + Assert.AreEqual("45", _shellStream.Read()); // Passes, but should probably fail and return "" + + // Case 2 above. + Assert.AreEqual("Hello World!\r\n12345", _shellStream.Expect("123")); // Passes + Assert.AreEqual("", _shellStream.Read()); // Fails, returns "45" + } + + [TestMethod] + public void Read_MultiByte() + { + _channelSessionStub.Receive(new byte[] { 0xF0 }); + _channelSessionStub.Receive(new byte[] { 0x9F }); + _channelSessionStub.Receive(new byte[] { 0x91 }); + _channelSessionStub.Receive(new byte[] { 0x8D }); + + Assert.AreEqual("πŸ‘", _shellStream.Read()); + } + + [TestMethod] + public void ReadLine_MultiByte() + { + _channelSessionStub.Receive(new byte[] { 0xF0 }); + _channelSessionStub.Receive(new byte[] { 0x9F }); + _channelSessionStub.Receive(new byte[] { 0x91 }); + _channelSessionStub.Receive(new byte[] { 0x8D }); + _channelSessionStub.Receive(new byte[] { 0x0D }); + _channelSessionStub.Receive(new byte[] { 0x0A }); + + Assert.AreEqual("πŸ‘", _shellStream.ReadLine()); + Assert.AreEqual("", _shellStream.Read()); + } + + [TestMethod] + [Ignore] + public void Expect_Regex_MultiByte() + { + _channelSessionStub.Receive(Encoding.UTF8.GetBytes("π“π“˜π“»π“˜π“»π“Ÿ π’»π“Ÿ")); + + Assert.AreEqual("π“π“˜π“»π“˜π“»π“Ÿ ", _shellStream.Expect(new Regex(@"\s"))); + Assert.AreEqual("π’»π“Ÿ", _shellStream.Read()); + } + + [TestMethod] + [Ignore] + public void Expect_String_MultiByte() + { + _channelSessionStub.Receive(Encoding.UTF8.GetBytes("hello δ½ ε₯½")); + + Assert.AreEqual("hello δ½ ε₯½", _shellStream.Expect("δ½ ε₯½")); + Assert.AreEqual("", _shellStream.Read()); + } + + [TestMethod] + public void Expect_Timeout() + { + Stopwatch stopwatch = Stopwatch.StartNew(); + + Assert.IsNull(_shellStream.Expect("Hello World!", TimeSpan.FromMilliseconds(200))); + + TimeSpan elapsed = stopwatch.Elapsed; + + // Account for variance in system timer resolution. + Assert.IsTrue(elapsed > TimeSpan.FromMilliseconds(180), elapsed.ToString()); + } + + private class ChannelSessionStub : IChannelSession + { + public void Receive(byte[] data) + { + DataReceived.Invoke(this, new ChannelDataEventArgs(channelNumber: 0, data)); + } + + public void Close() + { + Closed.Invoke(this, new ChannelEventArgs(channelNumber: 0)); + } + + public bool SendShellRequest() + { + return true; + } + + public bool SendPseudoTerminalRequest(string environmentVariable, uint columns, uint rows, uint width, uint height, IDictionary terminalModeValues) + { + return true; + } + + public void Dispose() + { + } + + public void Open() + { + } + + public event EventHandler DataReceived; + public event EventHandler Closed; +#pragma warning disable 0067 + public event EventHandler Exception; + public event EventHandler ExtendedDataReceived; + public event EventHandler RequestReceived; +#pragma warning restore 0067 + +#pragma warning disable IDE0025 // Use block body for property +#pragma warning disable IDE0022 // Use block body for method + public uint LocalChannelNumber => throw new NotImplementedException(); + + public uint LocalPacketSize => throw new NotImplementedException(); + + public uint RemotePacketSize => throw new NotImplementedException(); + + public bool IsOpen => throw new NotImplementedException(); + + public bool SendBreakRequest(uint breakLength) => throw new NotImplementedException(); + + public void SendData(byte[] data) => throw new NotImplementedException(); + + public void SendData(byte[] data, int offset, int size) => throw new NotImplementedException(); + + public bool SendEndOfWriteRequest() => throw new NotImplementedException(); + + public bool SendEnvironmentVariableRequest(string variableName, string variableValue) => throw new NotImplementedException(); + + public void SendEof() => throw new NotImplementedException(); + + public bool SendExecRequest(string command) => throw new NotImplementedException(); + + public bool SendExitSignalRequest(string signalName, bool coreDumped, string errorMessage, string language) => throw new NotImplementedException(); + + public bool SendExitStatusRequest(uint exitStatus) => throw new NotImplementedException(); + + public bool SendKeepAliveRequest() => throw new NotImplementedException(); + + public bool SendLocalFlowRequest(bool clientCanDo) => throw new NotImplementedException(); + + public bool SendSignalRequest(string signalName) => throw new NotImplementedException(); + + public bool SendSubsystemRequest(string subsystem) => throw new NotImplementedException(); + + public bool SendWindowChangeRequest(uint columns, uint rows, uint width, uint height) => throw new NotImplementedException(); + + public bool SendX11ForwardingRequest(bool isSingleConnection, string protocol, byte[] cookie, uint screenNumber) => throw new NotImplementedException(); +#pragma warning restore IDE0022 // Use block body for method +#pragma warning restore IDE0025 // Use block body for property + } + } +}