Skip to content

Commit 501e340

Browse files
committed
on second thought, open the file lazily in ReadLines
1 parent 0305f88 commit 501e340

File tree

2 files changed

+58
-69
lines changed

2 files changed

+58
-69
lines changed

src/Renci.SshNet/SftpClient.cs

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1803,9 +1803,13 @@ public string ReadAllText(string path, Encoding encoding)
18031803
/// <returns>
18041804
/// The lines of the file.
18051805
/// </returns>
1806-
/// <exception cref="ArgumentNullException"><paramref name="path"/> is <see langword="null"/>.</exception>
1807-
/// <exception cref="SshConnectionException">Client is not connected.</exception>
1808-
/// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
1806+
/// <remarks>
1807+
/// The lines are enumerated lazily. The opening of the file and any resulting exceptions occur
1808+
/// upon enumeration.
1809+
/// </remarks>
1810+
/// <exception cref="ArgumentNullException"><paramref name="path"/> is <see langword="null"/>. Thrown eagerly.</exception>
1811+
/// <exception cref="SshConnectionException">Client is not connected upon enumeration.</exception>
1812+
/// <exception cref="ObjectDisposedException">The return value is enumerated after the client is disposed.</exception>
18091813
public IEnumerable<string> ReadLines(string path)
18101814
{
18111815
return ReadLines(path, Encoding.UTF8);
@@ -1819,33 +1823,34 @@ public IEnumerable<string> ReadLines(string path)
18191823
/// <returns>
18201824
/// The lines of the file.
18211825
/// </returns>
1822-
/// <exception cref="ArgumentNullException"><paramref name="path"/> is <see langword="null"/>.</exception>
1823-
/// <exception cref="SshConnectionException">Client is not connected.</exception>
1824-
/// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
1826+
/// <remarks>
1827+
/// The lines are enumerated lazily. The opening of the file and any resulting exceptions occur
1828+
/// upon enumeration.
1829+
/// </remarks>
1830+
/// <exception cref="ArgumentNullException"><paramref name="path"/> is <see langword="null"/>. Thrown eagerly.</exception>
1831+
/// <exception cref="SshConnectionException">Client is not connected upon enumeration.</exception>
1832+
/// <exception cref="ObjectDisposedException">The return value is enumerated after the client is disposed.</exception>
18251833
public IEnumerable<string> ReadLines(string path, Encoding encoding)
18261834
{
1827-
// We open the file eagerly i.e. outside of the state machine created by yield,
1828-
// in order to a) throw resulting (e.g. file-related) exceptions eagerly; and b)
1829-
// to match what File.ReadLines does.
1830-
// This probably makes it behave more predictably/closer to what most people expect.
1831-
// The downside is that if the return value is never enumerated, the file
1832-
// is never closed (we can't do "using" here because it would be disposed
1833-
// as soon as we return). This conundrum also exists with File.ReadLines.
1834-
1835-
var sr = new StreamReader(OpenRead(path), encoding);
1835+
// We allow this usage exception to throw eagerly...
1836+
ThrowHelper.ThrowIfNull(path);
18361837

1837-
return Enumerate(sr);
1838+
// ... but other exceptions will throw lazily i.e. inside the state machine created
1839+
// by yield. We could choose to open the file eagerly as well in order to throw
1840+
// file-related exceptions eagerly (matching what File.ReadLines does), but this
1841+
// complicates double enumeration, and introduces the problem that File.ReadLines
1842+
// has whereby the file is not closed if the return value is not enumerated.
1843+
return Enumerate();
18381844

1839-
static IEnumerable<string> Enumerate(StreamReader sr)
1845+
IEnumerable<string> Enumerate()
18401846
{
1841-
using (sr)
1842-
{
1843-
string? line;
1847+
using var sr = new StreamReader(OpenRead(path), encoding);
18441848

1845-
while ((line = sr.ReadLine()) != null)
1846-
{
1847-
yield return line;
1848-
}
1849+
string? line;
1850+
1851+
while ((line = sr.ReadLine()) != null)
1852+
{
1853+
yield return line;
18491854
}
18501855
}
18511856
}

test/Renci.SshNet.IntegrationTests/SftpTests.cs

Lines changed: 29 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1788,6 +1788,9 @@ public void Sftp_ReadLines_NoEncoding_ExistingFile()
17881788

17891789
var actualLines = client.ReadLines(remoteFile);
17901790
Assert.IsNotNull(actualLines);
1791+
1792+
// These two lines together test double enumeration.
1793+
Assert.AreEqual(lines[0], actualLines.First());
17911794
CollectionAssert.AreEqual(lines, actualLines.ToArray());
17921795
}
17931796
finally
@@ -1801,7 +1804,9 @@ public void Sftp_ReadLines_NoEncoding_ExistingFile()
18011804
}
18021805

18031806
[TestMethod]
1804-
public void Sftp_ReadLines_NoEncoding_FileDoesNotExist()
1807+
[DataRow(false)]
1808+
[DataRow(true)]
1809+
public void Sftp_ReadLines_FileDoesNotExist(bool encoding)
18051810
{
18061811
using (var client = new SftpClient(_connectionInfoFactory.Create()))
18071812
{
@@ -1814,13 +1819,28 @@ public void Sftp_ReadLines_NoEncoding_FileDoesNotExist()
18141819
client.DeleteFile(remoteFile);
18151820
}
18161821

1822+
// This exception should bubble up immediately
1823+
var nullEx = encoding
1824+
? Assert.Throws<ArgumentNullException>(() => client.ReadLines(null, GetRandomEncoding()))
1825+
: Assert.Throws<ArgumentNullException>(() => client.ReadLines(null));
1826+
1827+
Assert.AreEqual("path", nullEx.ParamName);
1828+
18171829
try
18181830
{
1819-
client.ReadLines(remoteFile);
1820-
Assert.Fail();
1821-
}
1822-
catch (SftpPathNotFoundException ex)
1823-
{
1831+
var ex = Assert.ThrowsExactly<SftpPathNotFoundException>(() =>
1832+
{
1833+
// The PathNotFound exception is permitted to bubble up only upon
1834+
// enumerating.
1835+
var lines = encoding
1836+
? client.ReadLines(remoteFile, GetRandomEncoding())
1837+
: client.ReadLines(remoteFile);
1838+
1839+
using var enumerator = lines.GetEnumerator();
1840+
1841+
_ = enumerator.MoveNext();
1842+
});
1843+
18241844
Assert.IsNull(ex.InnerException);
18251845
Assert.AreEqual("No such file", ex.Message);
18261846

@@ -1866,46 +1886,10 @@ public void Sftp_ReadLines_Encoding_ExistingFile()
18661886

18671887
var actualLines = client.ReadLines(remoteFile, encoding);
18681888
Assert.IsNotNull(actualLines);
1869-
CollectionAssert.AreEqual(lines, actualLines.ToArray());
1870-
}
1871-
finally
1872-
{
1873-
if (client.Exists(remoteFile))
1874-
{
1875-
client.DeleteFile(remoteFile);
1876-
}
1877-
}
1878-
}
1879-
}
1880-
1881-
[TestMethod]
1882-
public void Sftp_ReadLines_Encoding_FileDoesNotExist()
1883-
{
1884-
var encoding = GetRandomEncoding();
1885-
1886-
using (var client = new SftpClient(_connectionInfoFactory.Create()))
1887-
{
1888-
client.Connect();
1889-
1890-
var remoteFile = GenerateUniqueRemoteFileName();
1891-
1892-
if (client.Exists(remoteFile))
1893-
{
1894-
client.DeleteFile(remoteFile);
1895-
}
18961889

1897-
try
1898-
{
1899-
client.ReadLines(remoteFile, encoding);
1900-
Assert.Fail();
1901-
}
1902-
catch (SftpPathNotFoundException ex)
1903-
{
1904-
Assert.IsNull(ex.InnerException);
1905-
Assert.AreEqual("No such file", ex.Message);
1906-
1907-
// ensure file was not created by us
1908-
Assert.IsFalse(client.Exists(remoteFile));
1890+
// These two lines together test double enumeration.
1891+
Assert.AreEqual(lines[0], actualLines.First());
1892+
CollectionAssert.AreEqual(lines, actualLines.ToArray());
19091893
}
19101894
finally
19111895
{

0 commit comments

Comments
 (0)