Skip to content

Add tests and benchmarks for ShellStream.Read and Expect #1313

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions test/Renci.SshNet.IntegrationBenchmarks/SshClientBenchmark.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using BenchmarkDotNet.Attributes;

using Renci.SshNet.Common;
using Renci.SshNet.IntegrationTests.TestsFixtures;

namespace Renci.SshNet.IntegrationBenchmarks
Expand All @@ -8,6 +9,11 @@ namespace Renci.SshNet.IntegrationBenchmarks
[SimpleJob]
public class SshClientBenchmark : IntegrationBenchmarkBase
{
private static readonly Dictionary<TerminalModes, uint> ShellStreamTerminalModes = new Dictionary<TerminalModes, uint>
{
{ TerminalModes.ECHO, 0 }
};

private readonly InfrastructureFixture _infrastructureFixture;
private SshClient? _sshClient;

Expand Down Expand Up @@ -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:");
}
}
}
}
329 changes: 329 additions & 0 deletions test/Renci.SshNet.Tests/Classes/ShellStreamTest_ReadExpect.cs
Original file line number Diff line number Diff line change
@@ -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<IConnectionInfo>();

connectionInfoMock.Setup(p => p.Encoding).Returns(Encoding.UTF8);

var sessionMock = new Mock<ISession>();

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<byte>(), 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<TerminalModes, uint> terminalModeValues)
{
return true;
}

public void Dispose()
{
}

public void Open()
{
}

public event EventHandler<ChannelDataEventArgs> DataReceived;
public event EventHandler<ChannelEventArgs> Closed;
#pragma warning disable 0067
public event EventHandler<ExceptionEventArgs> Exception;
public event EventHandler<ChannelExtendedDataEventArgs> ExtendedDataReceived;
public event EventHandler<ChannelRequestEventArgs> 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
}
}
}