diff --git a/readme.md b/readme.md index d51ef58..248ac11 100644 --- a/readme.md +++ b/readme.md @@ -6,10 +6,10 @@ This library lets you read text input from STDIN that is entered from the consol ## Consuming the Library -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 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. +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 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. > **How To Configure NuGet Sources** > @@ -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 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<string>. 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<string> 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<string> 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<string> 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<string> as a constructor parameter, so it basically just wraps another collection as-is. Example: + +```csharp +ITextSource stdin = new StdInTextSource(); +stdin.Open(); +List sortedStdIn = new List(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<string> 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. \ No newline at end of file diff --git a/source/DotnetStandardStreams/AnonOutputTarget.cs b/source/DotnetStandardStreams/AnonOutputTarget.cs index aee2353..985d02a 100644 --- a/source/DotnetStandardStreams/AnonOutputTarget.cs +++ b/source/DotnetStandardStreams/AnonOutputTarget.cs @@ -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 outputProc; + public AnonOutputTarget(Action 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); } } } diff --git a/source/DotnetStandardStreams/BinaryStdinReader.cs b/source/DotnetStandardStreams/BinaryStdinReader.cs index e1f34c8..78553de 100644 --- a/source/DotnetStandardStreams/BinaryStdinReader.cs +++ b/source/DotnetStandardStreams/BinaryStdinReader.cs @@ -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? dataReceiverProc; protected readonly Action? doneProc; public event BytesReadEventHandler? OnBytesRead; public event EventHandler? OnDone; + public BinaryStdinReader(Action dataReceiverProc, Action doneProc) { this.dataReceiverProc = dataReceiverProc; this.doneProc = doneProc; } - public BinaryStdinReader() - { - } + public BinaryStdinReader() { } public void ReadBytes(Action receiverProc, Action 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)); diff --git a/source/DotnetStandardStreams/ConsoleOutputTarget.cs b/source/DotnetStandardStreams/ConsoleOutputTarget.cs index a1862a8..dacdcb9 100644 --- a/source/DotnetStandardStreams/ConsoleOutputTarget.cs +++ b/source/DotnetStandardStreams/ConsoleOutputTarget.cs @@ -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() { } } } diff --git a/source/DotnetStandardStreams/DotnetStandardStreams.csproj b/source/DotnetStandardStreams/DotnetStandardStreams.csproj index d1fd65a..07a57b2 100644 --- a/source/DotnetStandardStreams/DotnetStandardStreams.csproj +++ b/source/DotnetStandardStreams/DotnetStandardStreams.csproj @@ -1,7 +1,7 @@ - net5.0 + netstandard2.1;net5.0;net6.0;net7.0;net8.0;net9.0 enable true Phil Gilmore diff --git a/source/DotnetStandardStreams/FileTextSource.cs b/source/DotnetStandardStreams/FileTextSource.cs index 14cea55..c95c217 100644 --- a/source/DotnetStandardStreams/FileTextSource.cs +++ b/source/DotnetStandardStreams/FileTextSource.cs @@ -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 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 readAction) + { + foreach (string line in ReadAll()) + readAction(line); + } } -} +} \ No newline at end of file diff --git a/source/DotnetStandardStreams/ITextSource.cs b/source/DotnetStandardStreams/ITextSource.cs index 2558b72..3eeaca2 100644 --- a/source/DotnetStandardStreams/ITextSource.cs +++ b/source/DotnetStandardStreams/ITextSource.cs @@ -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 ReadAll(); + void ReadAll(Action readAction); string Read(); bool Eof(); void Close(); diff --git a/source/DotnetStandardStreams/ListTextSource.cs b/source/DotnetStandardStreams/ListTextSource.cs index 9388a03..a3527d9 100644 --- a/source/DotnetStandardStreams/ListTextSource.cs +++ b/source/DotnetStandardStreams/ListTextSource.cs @@ -11,53 +11,33 @@ namespace DotnetStandardStreams private bool isEof; private readonly IEnumerable source; private IEnumerator? enumerator; - //private string lastValue; - private bool firstIsRead; - private string nextLine; + public ListTextSource(IEnumerable 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 ReadAll() - { - return source.AsEnumerable(); - } + public virtual IEnumerable 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 readAction) + { + foreach (string line in ReadAll()) + readAction(line); + } } } diff --git a/source/DotnetStandardStreams/StdInTextSource.cs b/source/DotnetStandardStreams/StdInTextSource.cs index a9aa4f9..eb58bd9 100644 --- a/source/DotnetStandardStreams/StdInTextSource.cs +++ b/source/DotnetStandardStreams/StdInTextSource.cs @@ -17,21 +17,24 @@ namespace DotnetStandardStreams public virtual IEnumerable 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 readAction) { + foreach (string line in ReadAll()) + readAction(line); } } } diff --git a/source/DotnetStandardStreamsApp/Program.cs b/source/DotnetStandardStreamsApp/Program.cs index d518cea..171dd5e 100644 --- a/source/DotnetStandardStreamsApp/Program.cs +++ b/source/DotnetStandardStreamsApp/Program.cs @@ -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; + +/// +/// Typical command line for testing in PowerShell: +/// @(1,2,3,4,5,6,7,8,9,10) | .\DotnetStandardStreamsApp.exe .\input.txt +/// +public static class Program { - class Program + public static void Main(string[] args) { - - static void Main(string[] args) + ITextSource textSource; + 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(); - - 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(); } } diff --git a/source/DotnetStandardStreamsTests/AnonOutputTargetTests.cs b/source/DotnetStandardStreamsTests/AnonOutputTargetTests.cs new file mode 100644 index 0000000..6949633 --- /dev/null +++ b/source/DotnetStandardStreamsTests/AnonOutputTargetTests.cs @@ -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 expected = ["1", "2", "", "4", "5", "6", "7", "8", "9", "10"]; + List 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 input = ["1", "2", "", "4", "5", "6", "7", "8", "9", "10"]; + List expected = ["2", "4", "6", "8", "10"]; + List 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); + } +} diff --git a/source/DotnetStandardStreamsTests/ConsoleOutputTargetTests.cs b/source/DotnetStandardStreamsTests/ConsoleOutputTargetTests.cs index f274882..53640b3 100644 --- a/source/DotnetStandardStreamsTests/ConsoleOutputTargetTests.cs +++ b/source/DotnetStandardStreamsTests/ConsoleOutputTargetTests.cs @@ -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"); diff --git a/source/DotnetStandardStreamsTests/DotnetStandardStreamsTests.csproj b/source/DotnetStandardStreamsTests/DotnetStandardStreamsTests.csproj index 0d27252..c46cc9d 100644 --- a/source/DotnetStandardStreamsTests/DotnetStandardStreamsTests.csproj +++ b/source/DotnetStandardStreamsTests/DotnetStandardStreamsTests.csproj @@ -2,7 +2,7 @@ net8.0 - + enable false diff --git a/source/DotnetStandardStreamsTests/FileTextSourceTests.cs b/source/DotnetStandardStreamsTests/FileTextSourceTests.cs new file mode 100644 index 0000000..04b57a2 --- /dev/null +++ b/source/DotnetStandardStreamsTests/FileTextSourceTests.cs @@ -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 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 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 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 lines = textSource.ReadAll().ToList(); + int lineCount = lines.Count; + lineCount.ShouldBe(0); + }, + CreateEmptyInputFile); + } +} diff --git a/source/DotnetStandardStreamsTests/GlobalUsings.cs b/source/DotnetStandardStreamsTests/GlobalUsings.cs new file mode 100644 index 0000000..4a2bc7a --- /dev/null +++ b/source/DotnetStandardStreamsTests/GlobalUsings.cs @@ -0,0 +1,2 @@ +global using Xunit; +global using Shouldly; diff --git a/source/DotnetStandardStreamsTests/ListOutputTargetTests.cs b/source/DotnetStandardStreamsTests/ListOutputTargetTests.cs index 865fa36..a56cd6a 100644 --- a/source/DotnetStandardStreamsTests/ListOutputTargetTests.cs +++ b/source/DotnetStandardStreamsTests/ListOutputTargetTests.cs @@ -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()); - target.Output("1"); - target.Output("2"); - target.Output(""); - target.Output("3"); + ListOutputTarget target = new(new List()); + 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"); } -} +} \ No newline at end of file diff --git a/source/DotnetStandardStreamsTests/ListTextSourceTests.cs b/source/DotnetStandardStreamsTests/ListTextSourceTests.cs index 8130eb4..39cf4ef 100644 --- a/source/DotnetStandardStreamsTests/ListTextSourceTests.cs +++ b/source/DotnetStandardStreamsTests/ListTextSourceTests.cs @@ -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); + } +} \ No newline at end of file diff --git a/source/DotnetStandardStreamsTests/StdInTextSourceTests.cs b/source/DotnetStandardStreamsTests/StdInTextSourceTests.cs index 1f3791f..c02c337 100644 --- a/source/DotnetStandardStreamsTests/StdInTextSourceTests.cs +++ b/source/DotnetStandardStreamsTests/StdInTextSourceTests.cs @@ -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 testCode) + private static TextReader CreateEmptyStdIn() => new ListTextReader([]); + + private static void WrapStdInTest(Action 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 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 lines = stdin.ReadAll().ToList(); + int lineCount = lines.Count; + lineCount.ShouldBe(0); + }, + CreateEmptyStdIn()); } } diff --git a/source/DotnetStandardStreamsTests/Testables/ListTextReader.cs b/source/DotnetStandardStreamsTests/Testables/ListTextReader.cs index c7d8e6e..97a507a 100644 --- a/source/DotnetStandardStreamsTests/Testables/ListTextReader.cs +++ b/source/DotnetStandardStreamsTests/Testables/ListTextReader.cs @@ -15,7 +15,7 @@ namespace DotnetStandardStreamsTests.Testables enumerator = data.GetEnumerator(); } - public override string ReadLine() + public override string? ReadLine() { if (enumerator.MoveNext()) return enumerator.Current;