Fixed EOF order issues. Many fixes for bugs, hints, warnings. Added ReadAll(Action) method. Updated Readme.

This commit is contained in:
Phil Gilmore 2025-06-24 14:59:06 -06:00
parent cde6dc865c
commit 117df18c56
19 changed files with 760 additions and 386 deletions

209
readme.md
View File

@ -37,9 +37,9 @@ Add a new Package Source.
## Usage Instructions
### Consuming Application
### The Consuming Application
Your console app can use DotnetStreams to enable usage such as these:
Your console app can use DotnetStreams to enable usage such as the following examples, using a single approach in code regardless of the source of your input:
```powershell
echo "This is a test." | myconsoleapp.exe
@ -49,6 +49,14 @@ echo "This is a test." | myconsoleapp.exe
type myfile.txt | myconsoleapp.exe
```
```powershell
myconsoleapp.exe --filename myfile.txt
```
```powershell
myconsoleapp.exe "line1" "line2" "line3"
```
```powershell
c:> myconsoleapp.exe
line 1
@ -58,9 +66,101 @@ line 3
^Z
```
The above scenarios can all be handled in a single application in same way using an interface. It may look something like this:
```csharp
ITextSource source;
if (args[0].Equals("--filename"))
source = new FileTextSource(args[1]);
else if (args.Length > 0)
source = new ListTextSource(args);
else
source = new StdInTextSource();
```
### ITextSource
ITextSource is the abstraction of the available kinds of inputs.
Implementations of ITextSource have `Open()` and `Close()` methods. They must be called for the implementations to operate properly. The `Close()` method should always be in a `finally` block.
*Example:*
```csharp
ITextSource source = ... ;
source.Open();
try
{
IEnumerable<string> lines = source.ReadAll();
}
finally
{
source.Close();
}
```
### IOutputTarget
Like ITextSource, implementations of IOutputTarget have `Open()` and `Close()` methods. They must be called for the implementations to operate properly. The `Close()` method should always be in a `finally` block.
```csharp
IOutputTarget outputTarget = ... ;
outputTarget.Open();
try
{
outputTarget.Output("Hello World");
}
finally
{
outputTarget.Close();
}
```
### Several Sources to Read
#### ITextSource
As described above, this is the interface that all text readers implement. It abstracts the source and the EOF behavior for uniform consumption of text sources. It has `Open()` and `Close()` fixture methods for implementations that need them.
#### StdInTextSource
This reads lines of text from the StdIn stream. This means that data can come from the Keyboard or can be piped in through the command line using the `|` pipe character using a `type` command to provide the contents of a file or using `write-output` (PowerShell) or `echo` (Cmd) to provide a literal text string from the command line. StdIn can also be redevined outside of the application in interesting ways.
#### FileTextSource
This reads lines of text from a text file on disk. You will need to provide your own mechanism for determining when to use it and what the filename will be. For example, in this document most examples demonstrate doing this by passing the filename as a command-line parameter which I think seems quite obvious.
#### ListTextSource
This wraps an IEnumerable&lt;string&gt;. This means it can also wrap a list. That, in turn, means you can wrap another ITextSource. This was written for experimentation and unit testing but may have other creative uses, such as transforming input sources by sorting the data, for example.
### Several Targets to Write
#### IOutputTarget
As described above, this is a simple interface that wraps an output destination. It has `Open()` and `Close()` methods to match the ITextSource and an `Output()` method to do the writing. The `Output()` method does NOT insert or append newlines or other line endings. Since you probably already have them in your strings from their original ITextSource or other processing, you won't usually need them. If your strings do not have line endings but need them, you must concatenate them yourself.
#### ListOutputTarget
This wraps an IList&lt;string&gt; and will store data to it when Output() is called. This is useful for unit testing or other creative workarounds.
#### ConsoleOutputTarget
This writes data to the StdOut device when Output() is called.
#### AnonOutputTarget
This will take an Action&lt;string&gt; parameter in its constructor to instruct it how to behave when Output() is called. This implementation can easily be used to fulfill any output need. Other obvious OutputTarget types (e.g. TextFileOutputTarget) have not been implemented because this method is simpler to use than those classes would be and it gives you more control than you would have with those classes. For example, you can control the access parameters when opening a text file.
*Example:*
```csharp
// Writes to the console.
IOutputTarget target = new AnonOutputTarget(static s =>
{
int maxLen = 15;
string truncatedString = s.Substring(0, Math.Min(s.Length, maxLen))
string logLine = $"[{DateTime.Now}] {truncatedString}";
Console.WriteLine(logLine);
};
target.Output("Super Califragilistic Expiyallydocious!");
```
### Code Usage
```powershell
```csharp
static void Main(string[] args)
{
ITextSource textSource;
@ -88,3 +188,106 @@ public static void Execute(ITextSource source, IOutputTarget target)
source.Close();
}
```
### Several Ways To Read
#### Iterator
You can read all lines using an iterator:
```csharp
foreach(string line in source.ReadAll())
// Do stuff with line here.
```
#### Callback
You can read all lines using a callback:
```csharp
source.ReadAll(static line => /*do stuff with line here*/);
```
#### Input / Output Method Group
The action callback version of the ReadAll() method is compatible with the delegate for IOutputTarget.Output(). Therefore, you can pass an IOutputTarget.Output method directly as a parameter to the ReadAll() method without wrapping it in a lambda function.
```csharp
ITextSource source = ... ;
IOutputTarget target = ... ;
source.Open();
target.Open();
// Notice .Output does not have parentheses and this is not using a lambda. We are passing the function directly.
source.ReadAll(target.Output);
target.Close();
source.Close();
```
#### One Line at a Time in a While Loop
You can read one line at a time in a loop, watching for EOF.
Be careful to check for EOF in this specific way. The text source implementations are specifically engineered to behave with this logic:
EOF is FALSE after each read that returns data.
EOF is TRUE after the first and subsequent reads where data is exhausted.
```csharp
ITextSource source = ... ;
string line = source.Read();
while (!source.Eof())
{
// Do stuff with line here.
string line = source.Read();
}
```
#### One Line at a Time in a FOR Loop
```csharp
source.Open();
target.Open();
for (string? line = source.Read(); !source.Eof(); line = source.Read())
{
target.Output(line);
}
target.Close();
source.Close();
```
#### ListTextSource
This is a text source that wraps an in-memory List&lt;string&gt; object. It is useful for unit testing or for creative workarounds. This is also useful as if you want to use it to wrap input from other text sources, such as a FileTextSource when you want to do document-level operations, such as sorting all the lines, which require all lines to be available at once instead of one at a time in an iterator pattern as is provided by the text source itself. It takes an IEnumerable&lt;string&gt; as a constructor parameter, so it basically just wraps another collection as-is. Example:
```csharp
ITextSource stdin = new StdInTextSource();
stdin.Open();
List<string> sortedStdIn = new List<string>(stdin.ReadAll());
sortedStdIn.Sort();
stdin.Close();
ITextSource listSource = new ListTextSource(sortedStdIn);
listSource.Open();
listSource.ReadAll(static line => { /*Do stuff with line here.*/ });
listSource.Close();
```
## Why not IDisposable?
At its inception, there were no ITextSource or IOutputTarget implementations that had underlying IDisposable aspects. It wasn't until I added the FileTextSource that I encountered one.
Now that we have one, the intent is to unwind the Dispose() handling in that class, Modify ITextSource and IOutputTarget to inherit IDisposable, then modify all implementations to implement IDisposable. This will probably involve a base class to generalize the disposal code and also the ReadAll(Action&lt;string&gt; readAction) method implementation since it's repeated verbatim in all the ITextSource implementations.
This will be done in the next version.
## Why not IEnumerable?
This is being considered. The primary concern is that the operations must be Open()d and Close()d. Along those lines, the Open() and Close() may be reconsidered as well and consolidated into the constructor and Dispose() methods, getting rid of them altogether. The IEnumerable would then be freely enumerated. I will experiment with this in the next few versions.
## Why not Async?
Generally speaking, async operations should be used for I/O-bound or CPU-bound operations. These operations are not really CPU-bound as items can be iterated independently. They may be I/O-bound but that depends on the implementation. The nature of these operations generally puts them in a category of startup code, wherein you can't do much else until they have completed anyway.
I am still considering adding async versions but for now I am satisfied with synchronous operation.

View File

@ -1,30 +1,23 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DotnetStandardStreams
{
public class AnonOutputTarget : IOutputTarget
{
private readonly Action<string> outputProc;
public AnonOutputTarget(Action<string> outputProc)
{
this.outputProc = outputProc;
}
public virtual void Close()
{
}
public virtual void Close() { }
public virtual void Open()
{
}
public virtual void Open() { }
public virtual void Output(string line)
{
this.outputProc?.Invoke(line);
outputProc.Invoke(line);
}
}
}

View File

@ -6,39 +6,41 @@ namespace DotnetStandardStreams
{
public byte[] Data { get; }
public int Size { get; }
public BytesReadEventArgs(byte[] data, int size)
{
this.Data = data;
this.Size = size;
Data = data;
Size = size;
}
}
public delegate void BytesReadEventHandler(object sender, BytesReadEventArgs e);
// TODO: This class is unfinished, untested, unused. Ignore it or fix it but don't use it as-is. Notice there is no interface to it yet.
public class BinaryStdinReader
{
protected readonly Action<byte[], int>? dataReceiverProc;
protected readonly Action<int>? doneProc;
public event BytesReadEventHandler? OnBytesRead;
public event EventHandler? OnDone;
public BinaryStdinReader(Action<byte[], int> dataReceiverProc, Action<int> doneProc)
{
this.dataReceiverProc = dataReceiverProc;
this.doneProc = doneProc;
}
public BinaryStdinReader()
{
}
public BinaryStdinReader() { }
public void ReadBytes(Action<byte[], int> receiverProc, Action<int> doneProc)
{
using System.IO.Stream stdinStream = System.Console.OpenStandardInput();
int totalBytesRead = 0;
int bufferSize = 2048;
const int bufferSize = 2048;
byte[] buffer = new byte[bufferSize];
int bytesRead = stdinStream.Read(buffer, 0, bufferSize);
while (bytesRead > 0)
{
OnBytesRead?.Invoke(this, new BytesReadEventArgs(buffer, bytesRead));

View File

@ -1,22 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DotnetStandardStreams
{
public class ConsoleOutputTarget : IOutputTarget
{
public virtual void Open()
{
}
public virtual void Open() { }
public virtual void Output(string line)
{
Console.WriteLine(line);
}
public virtual void Close()
{
}
public virtual void Close() { }
}
}

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<TargetFrameworks>netstandard2.1;net5.0;net6.0;net7.0;net8.0;net9.0</TargetFrameworks>
<Nullable>enable</Nullable>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Authors>Phil Gilmore</Authors>

View File

@ -7,11 +7,17 @@ using System.IO;
namespace DotnetStandardStreams
{
// THE GOAL:
// READ will return NULL when you read after the last line. This is fine.
// EOF will return TRUE when you read the last line.
// We want EOF to return FALSE after you read the last line and TRUE when you read AFTER the last line.
public class FileTextSource : ITextSource
{
protected FileStream? file;
protected StreamReader? reader;
protected string filename;
protected string? lastLineRead = string.Empty; // Do NOT make this null at first.
public FileTextSource(string filename)
{
@ -28,31 +34,40 @@ namespace DotnetStandardStreams
public virtual IEnumerable<string> ReadAll()
{
string? line = reader?.ReadLine();
while (line != null)
string line = Read();
while (!Eof())
{
yield return line;
line = reader?.ReadLine();
line = Read();
}
}
public virtual string Read()
{
if (!Eof())
return reader?.ReadLine() ?? string.Empty;
{
string? result = reader?.ReadLine();
lastLineRead = result;
return result ?? string.Empty;
}
else
return string.Empty;
}
public virtual bool Eof()
{
return reader?.EndOfStream ?? true;
//return reader?.EndOfStream ?? true;
return lastLineRead == null;
}
public virtual void Close()
{
try
{
// TODO: Uh... why are we calling Flush() on a file-read operation?
file?.Flush();
}
finally
@ -67,5 +82,11 @@ namespace DotnetStandardStreams
}
}
}
public void ReadAll(Action<string> readAction)
{
foreach (string line in ReadAll())
readAction(line);
}
}
}

View File

@ -1,8 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DotnetStandardStreams
{
@ -10,6 +7,7 @@ namespace DotnetStandardStreams
{
void Open();
IEnumerable<string> ReadAll();
void ReadAll(Action<string> readAction);
string Read();
bool Eof();
void Close();

View File

@ -11,53 +11,33 @@ namespace DotnetStandardStreams
private bool isEof;
private readonly IEnumerable<string> source;
private IEnumerator<string>? enumerator;
//private string lastValue;
private bool firstIsRead;
private string nextLine;
public ListTextSource(IEnumerable<string> source)
{
this.source = source;
enumerator = null;
firstIsRead = false;
nextLine = string.Empty;
//firstIsRead = false;
//nextLine = string.Empty;
}
public virtual void Open()
{
}
public virtual void Open() { }
public virtual IEnumerable<string> ReadAll()
{
return source.AsEnumerable();
}
public virtual IEnumerable<string> ReadAll() => source.AsEnumerable();
public virtual string Read()
{
if (enumerator == null)
enumerator = source.GetEnumerator();
string thisLine;
if (!firstIsRead)
{
// Read the first, put it in the "last" buffer.
isEof = !enumerator.MoveNext();
nextLine = enumerator.Current;
firstIsRead = true;
}
thisLine = nextLine;
//string thisLine;
if (!isEof)
{
isEof = !enumerator.MoveNext();
if (!isEof)
nextLine = enumerator.Current;
else
nextLine = string.Empty;
}
return thisLine;
if (!isEof)
return enumerator.Current;
else
return string.Empty;
}
public virtual bool Eof() => isEof;
@ -67,5 +47,11 @@ namespace DotnetStandardStreams
enumerator = null;
isEof = false;
}
public void ReadAll(Action<string> readAction)
{
foreach (string line in ReadAll())
readAction(line);
}
}
}

View File

@ -17,21 +17,24 @@ namespace DotnetStandardStreams
public virtual IEnumerable<string> ReadAll()
{
string? s = Console.ReadLine();
while (s != null)
string s = Read();
while (!Eof())
{
yield return s;
s = Console.ReadLine();
s = Read();
}
isEof = true;
}
public virtual string Read()
{
string? s = Console.ReadLine();
if (s == null)
{
isEof = true;
return string.Empty;
}
else
@ -40,8 +43,12 @@ namespace DotnetStandardStreams
public virtual bool Eof() => isEof;
public virtual void Close()
public virtual void Close() { }
public void ReadAll(Action<string> readAction)
{
foreach (string line in ReadAll())
readAction(line);
}
}
}

View File

@ -1,87 +1,98 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using DotnetStandardStreams;
using Console = System.Console;
namespace StreamsTest
namespace DotnetStandardStreamsApp;
/// <summary>
/// Typical command line for testing in PowerShell:
/// @(1,2,3,4,5,6,7,8,9,10) | .\DotnetStandardStreamsApp.exe .\input.txt
/// </summary>
public static class Program
{
class Program
public static void Main(string[] args)
{
ITextSource textSource;
IOutputTarget textTarget = new ConsoleOutputTarget();
static void Main(string[] args)
if (args.Length > 0)
textSource = new FileTextSource(args[0]);
else
textSource = new StdInTextSource();
Console.WriteLine("---- ExecuteAll");
ExecuteAll(
textSource,
textTarget);
//Console.WriteLine("---- ExecuteAction");
//ExecuteAction(
// textSource,
// textTarget);
//Console.WriteLine("---- ExecuteWhileNotEof");
//ExecuteWhileNotEof(
// textSource,
// textTarget);
//Console.WriteLine("---- ExecuteForNotEof");
//ExecuteForNotEof(
// textSource,
// textTarget);
}
private static void ExecuteWhileNotEof(ITextSource source, IOutputTarget target)
{
source.Open();
target.Open();
while (!source.Eof())
{
//Program p = new();
ITextSource textSource;
IOutputTarget textTarget = new ConsoleOutputTarget();
if (args.Length > 0)
textSource = new FileTextSource(args[0]);
else
textSource = new StdInTextSource();
Execute(
textSource,
textTarget);
string thisLine = source.Read();
target.Output(thisLine);
}
// Works, but needs to be more compact.
//static void Main(string[] args)
//{
// Program p = new Program();
target.Close();
source.Close();
}
// ITextSource textSource = null;
private static void ExecuteForNotEof(ITextSource source, IOutputTarget target)
{
source.Open();
target.Open();
// if (args.Length > 0)
// {
// var filename = args[0];
// if (!File.Exists(filename))
// {
// Console.WriteLine($"File not found ({filename}).");
// Environment.Exit(1);
// }
// else
// textSource = new FileTextSource(filename);
// }
// else
// textSource = new StdInTextSource();
// p.Execute(
// textSource,
// new ConsoleOutputTarget());
//}
//public void ExecuteReadLine(string[] args)
//{
// string s = Console.ReadLine();
// // This terminates on blank lines, no surprise
// //while (!string.IsNullOrEmpty(s))
// while (s != null)
// {
// s = s.Replace("\r", "{CR}")
// .Replace("\n", "{LF}")
// .Replace("\t", "{TAB}");
// if (s == string.Empty)
// s = "{EMPTYSTRING}";
// Console.WriteLine($"{s}");
// s = Console.ReadLine();
// }
// if (s == null)
// Console.WriteLine("/s/ is null");
//}
public static void Execute(ITextSource source, IOutputTarget target)
for (string? line = source.Read(); !source.Eof(); line = source.Read())
{
source.Open();
target.Open();
foreach (string line in source.ReadAll())
target.Output(line);
target.Close();
source.Close();
target.Output(line);
}
target.Close();
source.Close();
}
private static void ExecuteAction(ITextSource source, IOutputTarget target)
{
source.Open();
target.Open();
source.ReadAll(target.Output);
target.Close();
source.Close();
}
private static void ExecuteAll(ITextSource source, IOutputTarget target)
{
source.Open();
target.Open();
foreach (string line in source.ReadAll())
target.Output(line);
target.Close();
source.Close();
}
}

View File

@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using DotnetStandardStreams;
namespace DotnetStandardStreamsTests;
public class AnonOutputTargetTests
{
[Fact]
public void CanConstruct()
{
_ = new AnonOutputTarget(static s => { });
}
[Fact]
public void WritesEachLine()
{
List<string> expected = ["1", "2", "", "4", "5", "6", "7", "8", "9", "10"];
List<string> actual = new();
IOutputTarget target = new AnonOutputTarget(s => actual.Add(s));
foreach (string s in expected)
target.Output(s);
actual.ShouldBe(expected);
}
[Fact]
public void OutputsConditionally()
{
List<string> input = ["1", "2", "", "4", "5", "6", "7", "8", "9", "10"];
List<string> expected = ["2", "4", "6", "8", "10"];
List<string> actual = new();
IOutputTarget target = new AnonOutputTarget(s =>
{
if (int.TryParse(s, out int i))
if (i % 2 == 0)
actual.Add(s);
});
foreach (string s in input)
target.Output(s);
actual.ShouldBe(expected);
}
}

View File

@ -1,25 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xunit;
using Shouldly;
using DotnetStandardStreams;
using System.IO;
using DotnetStandardStreamsTests.Testables;
namespace DotnetStandardStreamsTests
{
public class ConsoleOutputTargetTests
{
private IOutputTarget outputTarget;
[Fact]
public void WritesToOutputStream()
{
ListWriter writer = new();
outputTarget = new TestableConsoleOutputTarget(writer);
IOutputTarget outputTarget = new TestableConsoleOutputTarget(writer);
outputTarget.Open();
outputTarget.Output("1");

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<nullable>enable</nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>

View File

@ -0,0 +1,181 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using DotnetStandardStreams;
using DotnetStandardStreamsTests.Testables;
namespace DotnetStandardStreamsTests;
public class FileTextSourceTests
{
private string tempFilename { get; set; }
private void CleanupTempFile()
{
if (File.Exists(tempFilename))
File.Delete(tempFilename);
}
private void CreateInputFile()
{
tempFilename = System.IO.Path.GetTempFileName();
const string content = "1\r\n2\r\n\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\r\n10\r\n";
CleanupTempFile();
File.WriteAllText(tempFilename, content);
}
private void CreateEmptyInputFile()
{
tempFilename = System.IO.Path.GetTempFileName();
CleanupTempFile();
File.WriteAllText(tempFilename, string.Empty);
}
public void WrapFileTest(Action<ITextSource> testCode, Action? explicitSourceCreation = null)
{
explicitSourceCreation = explicitSourceCreation ?? CreateInputFile;
explicitSourceCreation();
try
{
ITextSource source = new FileTextSource(tempFilename);
source.Open();
try
{
testCode(source);
}
finally
{
source.Close();
}
}
finally
{
CleanupTempFile();
}
}
[Fact]
public void CanReadAllFromFile()
{
WrapFileTest(static textSource =>
{
List<string> actual = textSource.ReadAll().ToList();
actual.Count.ShouldBe(10);
actual.ShouldBe(["1", "2", "", "4", "5", "6", "7", "8", "9", "10"]);
});
}
[Fact]
public void CanReadIndividualLinesFromFile()
{
WrapFileTest(static textSource =>
{
string s;
s = textSource.Read();
textSource.Eof().ShouldBe(false);
s.ShouldBe("1");
s = textSource.Read();
textSource.Eof().ShouldBe(false);
s.ShouldBe("2");
s = textSource.Read();
textSource.Eof().ShouldBe(false);
s.ShouldBe("");
s = textSource.Read();
textSource.Eof().ShouldBe(false);
s.ShouldBe("4");
s = textSource.Read();
textSource.Eof().ShouldBe(false);
s.ShouldBe("5");
s = textSource.Read();
textSource.Eof().ShouldBe(false);
s.ShouldBe("6");
s = textSource.Read();
textSource.Eof().ShouldBe(false);
s.ShouldBe("7");
s = textSource.Read();
textSource.Eof().ShouldBe(false);
s.ShouldBe("8");
s = textSource.Read();
textSource.Eof().ShouldBe(false);
s.ShouldBe("9");
s = textSource.Read();
textSource.Eof().ShouldBe(false);
s.ShouldBe("10");
s = textSource.Read();
textSource.Eof().ShouldBe(true);
});
}
[Fact]
public void CanWhileLoopThroughFileNormally()
{
WrapFileTest(static textSource =>
{
int lineCount = 0;
_ = textSource.Read();
while (!textSource.Eof())
{
lineCount++;
_ = textSource.Read();
}
lineCount.ShouldBe(10);
});
}
[Fact]
public void CanReadAllThroughFileNormally()
{
WrapFileTest(static textSource =>
{
List<string> lines = textSource.ReadAll().ToList();
int lineCount = lines.Count;
lineCount.ShouldBe(10);
lines.ShouldBe(["1", "2", "", "4", "5", "6", "7", "8", "9", "10"]);
});
}
[Fact]
public void CanWhileLoopThroughEmptyFile()
{
WrapFileTest(
static textSource =>
{
int lineCount = 0;
_ = textSource.Read();
while (!textSource.Eof())
{
lineCount++;
_ = textSource.Read();
}
lineCount.ShouldBe(0);
},
CreateEmptyInputFile);
}
[Fact]
public void CanReadAllThroughEmptyFile()
{
WrapFileTest(static textSource =>
{
List<string> lines = textSource.ReadAll().ToList();
int lineCount = lines.Count;
lineCount.ShouldBe(0);
},
CreateEmptyInputFile);
}
}

View File

@ -0,0 +1,2 @@
global using Xunit;
global using Shouldly;

View File

@ -1,27 +1,23 @@
using DotnetStandardStreams;
using System;
using System.Collections.Generic;
using Xunit;
using Shouldly;
using DotnetStandardStreams;
namespace DotnetStandardStreamsTests
namespace DotnetStandardStreamsTests;
public class ListOutputTargetTests
{
public class ListOutputTargetTests
[Fact]
public void WritesToList()
{
[Fact]
public void WritesToList()
{
ListOutputTarget target = new(new List<string>());
target.Output("1");
target.Output("2");
target.Output("");
target.Output("3");
ListOutputTarget target = new(new List<string>());
target.Output("1");
target.Output("2");
target.Output("");
target.Output("3");
target.OutputList.Count.ShouldBe(4);
target.OutputList[0].ShouldBe("1");
target.OutputList[1].ShouldBe("2");
target.OutputList[2].ShouldBe(string.Empty);
target.OutputList[3].ShouldBe("3");
}
target.OutputList.Count.ShouldBe(4);
target.OutputList[0].ShouldBe("1");
target.OutputList[1].ShouldBe("2");
target.OutputList[2].ShouldBe(string.Empty);
target.OutputList[3].ShouldBe("3");
}
}

View File

@ -1,39 +1,45 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xunit;
using Shouldly;
using DotnetStandardStreams;
namespace DotnetStandardStreamsTests
namespace DotnetStandardStreamsTests;
public class ListTextSourceTests
{
public class ListTextSourceTests
[Fact]
public void ReadsFromList()
{
[Fact]
public void ReadsFromList()
{
ITextSource reader = new ListTextSource(new[] { "1", "2", "", "3" });
string s0 = reader.Read();
s0.ShouldNotBeNull();
s0.ShouldBe("1");
reader.Eof().ShouldBe(false);
ITextSource reader = new ListTextSource(["1", "2", "", "3"]);
string s0 = reader.Read();
s0.ShouldNotBeNull();
s0.ShouldBe("1");
reader.Eof().ShouldBe(false);
string s1 = reader.Read();
s1.ShouldNotBeNull();
s1.ShouldBe("2");
reader.Eof().ShouldBe(false);
string s1 = reader.Read();
s1.ShouldNotBeNull();
s1.ShouldBe("2");
reader.Eof().ShouldBe(false);
string s2 = reader.Read();
s2.ShouldNotBeNull();
s2.ShouldBe(string.Empty);
reader.Eof().ShouldBe(false);
string s2 = reader.Read();
s2.ShouldNotBeNull();
s2.ShouldBe(string.Empty);
reader.Eof().ShouldBe(false);
string s3 = reader.Read();
s3.ShouldNotBeNull();
s3.ShouldBe("3");
reader.Eof().ShouldBe(true);
}
string s3 = reader.Read();
s3.ShouldNotBeNull();
s3.ShouldBe("3");
reader.Eof().ShouldBe(false);
string s4 = reader.Read();
s4.ShouldBe("");
reader.Eof().ShouldBe(true);
}
[Fact]
public void ReadsFromEmptyList()
{
ITextSource reader = new ListTextSource([]);
string s0 = reader.Read();
//s0.ShouldBeNull();
reader.Eof().ShouldBe(true);
}
}

View File

@ -1,210 +1,148 @@
using DotnetStandardStreamsTests.Testables;
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xunit;
using Shouldly;
using DotnetStandardStreams;
using DotnetStandardStreamsTests.Testables;
namespace DotnetStandardStreamsTests
namespace DotnetStandardStreamsTests;
public class StdInTextSourceTests
{
public class StdInTextSourceTests
private static TextReader CreateStdIn()
{
private static TextReader CreateStdIn()
{
return new ListTextReader(new[] { "1", "2", "", "3" });
}
return new ListTextReader(new[] { "1", "2", "", "3" });
}
private static void WrapStdInTest(Action<ITextSource> testCode)
private static TextReader CreateEmptyStdIn() => new ListTextReader([]);
private static void WrapStdInTest(Action<ITextSource> testCode, TextReader? explicitStdIn = null)
{
if (explicitStdIn == null)
explicitStdIn = CreateStdIn();
var oldInputReader = System.Console.In;
try
{
var oldInputReader = System.Console.In;
System.Console.SetIn(explicitStdIn);
ITextSource stdin = new StdInTextSource();
stdin.Open();
try
{
TextReader reader = CreateStdIn();
System.Console.SetIn(reader);
ITextSource stdin = new StdInTextSource();
stdin.Open();
try
{
testCode?.Invoke(stdin);
}
finally
{
stdin.Close();
}
testCode?.Invoke(stdin);
}
finally
{
System.Console.SetIn(oldInputReader);
stdin.Close();
}
}
[Fact]
public void CanReadAllFromStandardIn()
finally
{
WrapStdInTest(stdin =>
{
var actual = stdin.ReadAll().ToList();
actual.Count.ShouldBe(4);
actual[0].ShouldBe("1");
actual[1].ShouldBe("2");
actual[2].ShouldBe(string.Empty);
actual[3].ShouldBe("3");
});
System.Console.SetIn(oldInputReader);
}
}
//{
// // Fake the StdIn
// var oldInputReader = System.Console.In;
// try
// {
// TextReader reader = CreateStdIn();
// System.Console.SetIn(reader);
// ITextSource stdin = new StdInTextSource();
// stdin.Open();
// try
// {
// var actual = stdin.ReadAll().ToList();
// actual.Count.ShouldBe(4);
// actual[0].ShouldBe("1");
// actual[1].ShouldBe("2");
// actual[2].ShouldBe(string.Empty);
// actual[3].ShouldBe("3");
// }
// finally
// {
// stdin.Close();
// }
// }
// finally
// {
// System.Console.SetIn(oldInputReader);
// }
//}
[Fact]
public void CanReadIndividualLinesFromStandardIn()
[Fact]
public void CanReadAllFromStandardIn()
{
WrapStdInTest(static stdin =>
{
WrapStdInTest(stdin =>
var actual = stdin.ReadAll().ToList();
actual.Count.ShouldBe(4);
actual[0].ShouldBe("1");
actual[1].ShouldBe("2");
actual[2].ShouldBe(string.Empty);
actual[3].ShouldBe("3");
});
}
[Fact]
public void CanReadIndividualLinesFromStandardIn()
{
WrapStdInTest(static stdin =>
{
string s;
s = stdin.Read();
stdin.Eof().ShouldBe(false);
s.ShouldBe("1");
s = stdin.Read();
stdin.Eof().ShouldBe(false);
s.ShouldBe("2");
s = stdin.Read();
stdin.Eof().ShouldBe(false);
s.ShouldBe(string.Empty);
s = stdin.Read();
stdin.Eof().ShouldBe(false);
s.ShouldBe("3");
s = stdin.Read();
stdin.Eof().ShouldBe(true);
});
}
[Fact]
public void CanWhileLoopThroughStdInNormally()
{
WrapStdInTest(static stdin =>
{
int lineCount = 0;
string s = stdin.Read();
while (!stdin.Eof())
{
string s;
lineCount++;
s = stdin.Read();
stdin.Eof().ShouldBe(false);
s.ShouldBe("1");
}
s = stdin.Read();
stdin.Eof().ShouldBe(false);
s.ShouldBe("2");
lineCount.ShouldBe(4);
});
}
s = stdin.Read();
stdin.Eof().ShouldBe(false);
s.ShouldBe(string.Empty);
s = stdin.Read();
stdin.Eof().ShouldBe(false);
s.ShouldBe("3");
s = stdin.Read();
stdin.Eof().ShouldBe(true);
});
//// Fake the StdIn
//var oldInputReader = System.Console.In;
//try
//{
// TextReader reader = CreateStdIn();
// System.Console.SetIn(reader);
// ITextSource stdin = new StdInTextSource();
// stdin.Open();
// try
// {
// string s;
// s = stdin.Read();
// stdin.Eof().ShouldBe(false);
// s.ShouldBe("1");
// s = stdin.Read();
// stdin.Eof().ShouldBe(false);
// s.ShouldBe("2");
// s = stdin.Read();
// stdin.Eof().ShouldBe(false);
// s.ShouldBe(string.Empty);
// s = stdin.Read();
// stdin.Eof().ShouldBe(false);
// s.ShouldBe("3");
// s = stdin.Read();
// stdin.Eof().ShouldBe(true);
// }
// finally
// {
// stdin.Close();
// }
//}
//finally
//{
// System.Console.SetIn(oldInputReader);
//}
}
[Fact]
public void CanWhileLoopThroughStdInProperly()
[Fact]
public void CanReadAllThroughStdInNormally()
{
WrapStdInTest(static stdin =>
{
WrapStdInTest(stdin =>
List<string> lines = stdin.ReadAll().ToList();
int lineCount = lines.Count;
lineCount.ShouldBe(4);
lines.ShouldBe(["1", "2", "", "3"]);
});
}
[Fact]
public void CanWhileLoopThroughStdInEmpty()
{
WrapStdInTest(
static stdin =>
{
int lineCount = 0;
var s = stdin.Read();
string s = stdin.Read();
while (!stdin.Eof())
{
lineCount++;
s = stdin.Read();
}
lineCount.ShouldBe(4);
});
}
lineCount.ShouldBe(0);
},
CreateEmptyStdIn());
}
//{
// // Fake the StdIn
// var oldInputReader = System.Console.In;
// try
// {
// TextReader reader = CreateStdIn();
// System.Console.SetIn(reader);
// ITextSource stdin = new StdInTextSource();
// stdin.Open();
// try
// {
// int lineCount = 0;
// var s = stdin.Read();
// while (!stdin.Eof())
// {
// // Do stuff with s here normally.
// lineCount++;
// s = stdin.Read();
// }
// lineCount.ShouldBe(4);
// }
// finally
// {
// stdin.Close();
// }
// }
// finally
// {
// System.Console.SetIn(oldInputReader);
// }
//}
[Fact]
public void CanReadAllThroughStdInEmpty()
{
WrapStdInTest(static stdin =>
{
List<string> lines = stdin.ReadAll().ToList();
int lineCount = lines.Count;
lineCount.ShouldBe(0);
},
CreateEmptyStdIn());
}
}

View File

@ -15,7 +15,7 @@ namespace DotnetStandardStreamsTests.Testables
enumerator = data.GetEnumerator();
}
public override string ReadLine()
public override string? ReadLine()
{
if (enumerator.MoveNext())
return enumerator.Current;