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

217
readme.md
View File

@ -6,10 +6,10 @@ This library lets you read text input from STDIN that is entered from the consol
## Consuming the Library ## Consuming the Library
You can use the library in several ways: You can use the library in several ways:
- By copying the code to your source repo and adding a project reference to the DotnetStreams project. - By copying the code to your source repo and adding a project reference to the DotnetStreams project.
- By adding a NuGet folder resource to your NuGet sources configuration which points to this project. - By adding a NuGet folder resource to your NuGet sources configuration which points to this project.
- By adding a NuGet reference to the DotnetStreams project in the repo at nuget.pillidar.com. - By adding a NuGet reference to the DotnetStreams project in the repo at nuget.pillidar.com.
> **How To Configure NuGet Sources** > **How To Configure NuGet Sources**
> >
@ -37,9 +37,9 @@ Add a new Package Source.
## Usage Instructions ## 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 ```powershell
echo "This is a test." | myconsoleapp.exe echo "This is a test." | myconsoleapp.exe
@ -49,6 +49,14 @@ echo "This is a test." | myconsoleapp.exe
type myfile.txt | myconsoleapp.exe type myfile.txt | myconsoleapp.exe
``` ```
```powershell
myconsoleapp.exe --filename myfile.txt
```
```powershell
myconsoleapp.exe "line1" "line2" "line3"
```
```powershell ```powershell
c:> myconsoleapp.exe c:> myconsoleapp.exe
line 1 line 1
@ -58,9 +66,101 @@ line 3
^Z ^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 ### Code Usage
```powershell ```csharp
static void Main(string[] args) static void Main(string[] args)
{ {
ITextSource textSource; ITextSource textSource;
@ -88,3 +188,106 @@ public static void Execute(ITextSource source, IOutputTarget target)
source.Close(); 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;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DotnetStandardStreams namespace DotnetStandardStreams
{ {
public class AnonOutputTarget : IOutputTarget public class AnonOutputTarget : IOutputTarget
{ {
private readonly Action<string> outputProc; private readonly Action<string> outputProc;
public AnonOutputTarget(Action<string> outputProc) public AnonOutputTarget(Action<string> outputProc)
{ {
this.outputProc = 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) 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 byte[] Data { get; }
public int Size { get; } public int Size { get; }
public BytesReadEventArgs(byte[] data, int size) public BytesReadEventArgs(byte[] data, int size)
{ {
this.Data = data; Data = data;
this.Size = size; Size = size;
} }
} }
public delegate void BytesReadEventHandler(object sender, BytesReadEventArgs e); 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 public class BinaryStdinReader
{ {
protected readonly Action<byte[], int>? dataReceiverProc; protected readonly Action<byte[], int>? dataReceiverProc;
protected readonly Action<int>? doneProc; protected readonly Action<int>? doneProc;
public event BytesReadEventHandler? OnBytesRead; public event BytesReadEventHandler? OnBytesRead;
public event EventHandler? OnDone; public event EventHandler? OnDone;
public BinaryStdinReader(Action<byte[], int> dataReceiverProc, Action<int> doneProc) public BinaryStdinReader(Action<byte[], int> dataReceiverProc, Action<int> doneProc)
{ {
this.dataReceiverProc = dataReceiverProc; this.dataReceiverProc = dataReceiverProc;
this.doneProc = doneProc; this.doneProc = doneProc;
} }
public BinaryStdinReader() public BinaryStdinReader() { }
{
}
public void ReadBytes(Action<byte[], int> receiverProc, Action<int> doneProc) public void ReadBytes(Action<byte[], int> receiverProc, Action<int> doneProc)
{ {
using System.IO.Stream stdinStream = System.Console.OpenStandardInput(); using System.IO.Stream stdinStream = System.Console.OpenStandardInput();
int totalBytesRead = 0; int totalBytesRead = 0;
int bufferSize = 2048; const int bufferSize = 2048;
byte[] buffer = new byte[bufferSize]; byte[] buffer = new byte[bufferSize];
int bytesRead = stdinStream.Read(buffer, 0, bufferSize); int bytesRead = stdinStream.Read(buffer, 0, bufferSize);
while (bytesRead > 0) while (bytesRead > 0)
{ {
OnBytesRead?.Invoke(this, new BytesReadEventArgs(buffer, bytesRead)); OnBytesRead?.Invoke(this, new BytesReadEventArgs(buffer, bytesRead));

View File

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

View File

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

View File

@ -7,11 +7,17 @@ using System.IO;
namespace DotnetStandardStreams 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 public class FileTextSource : ITextSource
{ {
protected FileStream? file; protected FileStream? file;
protected StreamReader? reader; protected StreamReader? reader;
protected string filename; protected string filename;
protected string? lastLineRead = string.Empty; // Do NOT make this null at first.
public FileTextSource(string filename) public FileTextSource(string filename)
{ {
@ -28,31 +34,40 @@ namespace DotnetStandardStreams
public virtual IEnumerable<string> ReadAll() public virtual IEnumerable<string> ReadAll()
{ {
string? line = reader?.ReadLine(); string line = Read();
while (line != null)
while (!Eof())
{ {
yield return line; yield return line;
line = reader?.ReadLine();
line = Read();
} }
} }
public virtual string Read() public virtual string Read()
{ {
if (!Eof()) if (!Eof())
return reader?.ReadLine() ?? string.Empty; {
string? result = reader?.ReadLine();
lastLineRead = result;
return result ?? string.Empty;
}
else else
return string.Empty; return string.Empty;
} }
public virtual bool Eof() public virtual bool Eof()
{ {
return reader?.EndOfStream ?? true; //return reader?.EndOfStream ?? true;
return lastLineRead == null;
} }
public virtual void Close() public virtual void Close()
{ {
try try
{ {
// TODO: Uh... why are we calling Flush() on a file-read operation?
file?.Flush(); file?.Flush();
} }
finally 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DotnetStandardStreams namespace DotnetStandardStreams
{ {
@ -10,6 +7,7 @@ namespace DotnetStandardStreams
{ {
void Open(); void Open();
IEnumerable<string> ReadAll(); IEnumerable<string> ReadAll();
void ReadAll(Action<string> readAction);
string Read(); string Read();
bool Eof(); bool Eof();
void Close(); void Close();

View File

@ -11,53 +11,33 @@ namespace DotnetStandardStreams
private bool isEof; private bool isEof;
private readonly IEnumerable<string> source; private readonly IEnumerable<string> source;
private IEnumerator<string>? enumerator; private IEnumerator<string>? enumerator;
//private string lastValue;
private bool firstIsRead;
private string nextLine;
public ListTextSource(IEnumerable<string> source) public ListTextSource(IEnumerable<string> source)
{ {
this.source = source; this.source = source;
enumerator = null; enumerator = null;
firstIsRead = false; //firstIsRead = false;
nextLine = string.Empty; //nextLine = string.Empty;
} }
public virtual void Open() public virtual void Open() { }
{
}
public virtual IEnumerable<string> ReadAll() public virtual IEnumerable<string> ReadAll() => source.AsEnumerable();
{
return source.AsEnumerable();
}
public virtual string Read() public virtual string Read()
{ {
if (enumerator == null) if (enumerator == null)
enumerator = source.GetEnumerator(); enumerator = source.GetEnumerator();
string thisLine; //string thisLine;
if (!firstIsRead)
{
// Read the first, put it in the "last" buffer.
isEof = !enumerator.MoveNext();
nextLine = enumerator.Current;
firstIsRead = true;
}
thisLine = nextLine;
if (!isEof) if (!isEof)
{
isEof = !enumerator.MoveNext(); isEof = !enumerator.MoveNext();
if (!isEof)
nextLine = enumerator.Current;
else
nextLine = string.Empty;
} if (!isEof)
return enumerator.Current;
return thisLine; else
return string.Empty;
} }
public virtual bool Eof() => isEof; public virtual bool Eof() => isEof;
@ -67,5 +47,11 @@ namespace DotnetStandardStreams
enumerator = null; enumerator = null;
isEof = false; 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() public virtual IEnumerable<string> ReadAll()
{ {
string? s = Console.ReadLine(); string s = Read();
while (s != null)
while (!Eof())
{ {
yield return s; yield return s;
s = Console.ReadLine();
s = Read();
} }
isEof = true;
} }
public virtual string Read() public virtual string Read()
{ {
string? s = Console.ReadLine(); string? s = Console.ReadLine();
if (s == null) if (s == null)
{ {
isEof = true; isEof = true;
return string.Empty; return string.Empty;
} }
else else
@ -40,8 +43,12 @@ namespace DotnetStandardStreams
public virtual bool Eof() => isEof; 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;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using DotnetStandardStreams; 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;
static void Main(string[] args) IOutputTarget textTarget = new ConsoleOutputTarget();
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(); string thisLine = source.Read();
target.Output(thisLine);
ITextSource textSource;
IOutputTarget textTarget = new ConsoleOutputTarget();
if (args.Length > 0)
textSource = new FileTextSource(args[0]);
else
textSource = new StdInTextSource();
Execute(
textSource,
textTarget);
} }
// Works, but needs to be more compact. target.Close();
//static void Main(string[] args) source.Close();
//{ }
// Program p = new Program();
// ITextSource textSource = null; private static void ExecuteForNotEof(ITextSource source, IOutputTarget target)
{
source.Open();
target.Open();
// if (args.Length > 0) for (string? line = source.Read(); !source.Eof(); line = source.Read())
// {
// 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)
{ {
source.Open(); target.Output(line);
target.Open();
foreach (string line in source.ReadAll())
target.Output(line);
target.Close();
source.Close();
} }
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;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xunit;
using Shouldly;
using DotnetStandardStreams; using DotnetStandardStreams;
using System.IO;
using DotnetStandardStreamsTests.Testables; using DotnetStandardStreamsTests.Testables;
namespace DotnetStandardStreamsTests namespace DotnetStandardStreamsTests
{ {
public class ConsoleOutputTargetTests public class ConsoleOutputTargetTests
{ {
private IOutputTarget outputTarget;
[Fact] [Fact]
public void WritesToOutputStream() public void WritesToOutputStream()
{ {
ListWriter writer = new(); ListWriter writer = new();
outputTarget = new TestableConsoleOutputTarget(writer); IOutputTarget outputTarget = new TestableConsoleOutputTarget(writer);
outputTarget.Open(); outputTarget.Open();
outputTarget.Output("1"); outputTarget.Output("1");

View File

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<nullable>enable</nullable>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
</PropertyGroup> </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;
using System.Collections.Generic; using System.Collections.Generic;
using Xunit; using DotnetStandardStreams;
using Shouldly;
namespace DotnetStandardStreamsTests namespace DotnetStandardStreamsTests;
public class ListOutputTargetTests
{ {
public class ListOutputTargetTests [Fact]
public void WritesToList()
{ {
[Fact] ListOutputTarget target = new(new List<string>());
public void WritesToList() target.Output("1");
{ target.Output("2");
ListOutputTarget target = new(new List<string>()); target.Output("");
target.Output("1"); target.Output("3");
target.Output("2");
target.Output("");
target.Output("3");
target.OutputList.Count.ShouldBe(4); target.OutputList.Count.ShouldBe(4);
target.OutputList[0].ShouldBe("1"); target.OutputList[0].ShouldBe("1");
target.OutputList[1].ShouldBe("2"); target.OutputList[1].ShouldBe("2");
target.OutputList[2].ShouldBe(string.Empty); target.OutputList[2].ShouldBe(string.Empty);
target.OutputList[3].ShouldBe("3"); target.OutputList[3].ShouldBe("3");
}
} }
} }

View File

@ -1,39 +1,45 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xunit;
using Shouldly;
using DotnetStandardStreams; using DotnetStandardStreams;
namespace DotnetStandardStreamsTests namespace DotnetStandardStreamsTests;
public class ListTextSourceTests
{ {
public class ListTextSourceTests [Fact]
public void ReadsFromList()
{ {
[Fact] ITextSource reader = new ListTextSource(["1", "2", "", "3"]);
public void ReadsFromList() string s0 = reader.Read();
{ s0.ShouldNotBeNull();
ITextSource reader = new ListTextSource(new[] { "1", "2", "", "3" }); s0.ShouldBe("1");
string s0 = reader.Read(); reader.Eof().ShouldBe(false);
s0.ShouldNotBeNull();
s0.ShouldBe("1");
reader.Eof().ShouldBe(false);
string s1 = reader.Read(); string s1 = reader.Read();
s1.ShouldNotBeNull(); s1.ShouldNotBeNull();
s1.ShouldBe("2"); s1.ShouldBe("2");
reader.Eof().ShouldBe(false); reader.Eof().ShouldBe(false);
string s2 = reader.Read(); string s2 = reader.Read();
s2.ShouldNotBeNull(); s2.ShouldNotBeNull();
s2.ShouldBe(string.Empty); s2.ShouldBe(string.Empty);
reader.Eof().ShouldBe(false); reader.Eof().ShouldBe(false);
string s3 = reader.Read(); string s3 = reader.Read();
s3.ShouldNotBeNull(); s3.ShouldNotBeNull();
s3.ShouldBe("3"); s3.ShouldBe("3");
reader.Eof().ShouldBe(true); 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.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xunit;
using Shouldly;
using DotnetStandardStreams; 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 try
{ {
TextReader reader = CreateStdIn(); testCode?.Invoke(stdin);
System.Console.SetIn(reader);
ITextSource stdin = new StdInTextSource();
stdin.Open();
try
{
testCode?.Invoke(stdin);
}
finally
{
stdin.Close();
}
} }
finally finally
{ {
System.Console.SetIn(oldInputReader); stdin.Close();
} }
} }
finally
[Fact]
public void CanReadAllFromStandardIn()
{ {
WrapStdInTest(stdin => System.Console.SetIn(oldInputReader);
{
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 CanReadAllFromStandardIn()
// // Fake the StdIn {
// var oldInputReader = System.Console.In; WrapStdInTest(static stdin =>
// 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()
{ {
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(); s = stdin.Read();
stdin.Eof().ShouldBe(false); }
s.ShouldBe("1");
s = stdin.Read(); lineCount.ShouldBe(4);
stdin.Eof().ShouldBe(false); });
s.ShouldBe("2"); }
s = stdin.Read(); [Fact]
stdin.Eof().ShouldBe(false); public void CanReadAllThroughStdInNormally()
s.ShouldBe(string.Empty); {
WrapStdInTest(static stdin =>
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()
{ {
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; int lineCount = 0;
var s = stdin.Read(); string s = stdin.Read();
while (!stdin.Eof()) while (!stdin.Eof())
{ {
lineCount++; lineCount++;
s = stdin.Read(); s = stdin.Read();
} }
lineCount.ShouldBe(4); lineCount.ShouldBe(0);
}); },
} CreateEmptyStdIn());
}
//{ [Fact]
// // Fake the StdIn public void CanReadAllThroughStdInEmpty()
// var oldInputReader = System.Console.In; {
// try WrapStdInTest(static stdin =>
// { {
// TextReader reader = CreateStdIn(); List<string> lines = stdin.ReadAll().ToList();
// System.Console.SetIn(reader); int lineCount = lines.Count;
lineCount.ShouldBe(0);
// ITextSource stdin = new StdInTextSource(); },
// stdin.Open(); CreateEmptyStdIn());
// 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);
// }
//}
} }
} }

View File

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