Skip to content

Commit

Permalink
SepWriter: Improve debuggability with DebuggerDisplay and Info plus n…
Browse files Browse the repository at this point in the history
…ew To* extensions (#257)

Fix codecov.yml issues, add validate-codecov.sh bash script
  • Loading branch information
nietras authored Feb 1, 2025
1 parent 427be77 commit eafedf6
Show file tree
Hide file tree
Showing 8 changed files with 204 additions and 44 deletions.
52 changes: 28 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,17 @@ and similar from [.NET 7+ and C#
and highly efficient implementation.

**🔎 Minimal** - a succinct yet expressive API with few options and no hidden
changes to input or output. What you read/write is what you get. E.g. by
default there is no "automatic" escaping/unescaping of quotes or trimming of
spaces. To enable this see [SepReaderOptions](#sepreaderoptions) and
changes to input or output. What you read/write is what you get. E.g. by default
there is no "automatic" escaping/unescaping of quotes or trimming of spaces. To
enable this see [SepReaderOptions](#sepreaderoptions) and
[Unescaping](#unescaping) and [Trimming](#trimming). See
[SepWriterOptions](#sepwriteroptions) for [Escaping](#escaping).

**🚀 Fast** - blazing fast with both architecture specific and cross-platform
SIMD vectorized parsing incl. 64/128/256/512-bit paths e.g. AVX2, AVX-512
(.NET 8.0+), NEON. Uses
[csFastFloat](https://github.com/CarlVerret/csFastFloat) for fast parsing of
floating points. See [detailed benchmarks](#comparison-benchmarks) for
cross-platform results.
SIMD vectorized parsing incl. 64/128/256/512-bit paths e.g. AVX2, AVX-512 (.NET
8.0+), NEON. Uses [csFastFloat](https://github.com/CarlVerret/csFastFloat) for
fast parsing of floating points. See [detailed
benchmarks](#comparison-benchmarks) for cross-platform results.

**🌪️ Multi-threaded** - unparalleled speed with highly efficient parallel CSV
parsing that is [up to 35x faster than
Expand All @@ -53,15 +52,16 @@ CsvHelper](#floats-reader-comparison-benchmarks), see

**🌀 Async support** - efficient `ValueTask` based `async/await` support.
Requires C# 13.0+ and for .NET 9.0+ includes `SepReader` implementing
`IAsyncEnumerable<>`. See [Async Support](#async-support) for details.
**🗑️ Zero allocation** - intelligent and efficient memory management allowing
for zero allocations after warmup incl. supporting use cases of reading or
writing arrays of values (e.g. features) easily without repeated allocations.

**✅ Thorough tests** - great code coverage and focus on edge case testing
incl. randomized [fuzz testing](https://en.wikipedia.org/wiki/Fuzzing).
**🌐 Cross-platform** - works on any platform, any architecture supported by
NET. 100% managed and written in beautiful modern C#.
`IAsyncEnumerable<>`. See [Async Support](#async-support) for details. **🗑️
Zero allocation** - intelligent and efficient memory management allowing for
zero allocations after warmup incl. supporting use cases of reading or writing
arrays of values (e.g. features) easily without repeated allocations.

**✅ Thorough tests** - great code coverage and focus on edge case testing incl.
randomized [fuzz testing](https://en.wikipedia.org/wiki/Fuzzing). **🌐
Cross-platform** - works on any platform, any architecture supported by NET.
100% managed and written in beautiful modern C#.

**✂️ Trimmable and AOT/NativeAOT compatible** - no problematic reflection or
dynamic code generation. Hence, fully
[trimmable](https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/prepare-libraries-for-trimming)
Expand Down Expand Up @@ -2226,6 +2226,7 @@ namespace nietras.SeparatedValues
AfterUnescape = 2,
All = 3,
}
[System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")]
public sealed class SepWriter : System.IAsyncDisposable, System.IDisposable
{
public nietras.SeparatedValues.SepWriterHeader Header { get; }
Expand Down Expand Up @@ -2298,13 +2299,16 @@ namespace nietras.SeparatedValues
}
public static class SepWriterExtensions
{
public static nietras.SeparatedValues.SepWriter To(this nietras.SeparatedValues.SepWriterOptions options, System.IO.Stream stream) { }
public static nietras.SeparatedValues.SepWriter To(this nietras.SeparatedValues.SepWriterOptions options, System.IO.TextWriter writer) { }
public static nietras.SeparatedValues.SepWriter To(this nietras.SeparatedValues.SepWriterOptions options, System.IO.Stream stream, bool leaveOpen) { }
public static nietras.SeparatedValues.SepWriter To(this nietras.SeparatedValues.SepWriterOptions options, System.IO.TextWriter writer, bool leaveOpen) { }
public static nietras.SeparatedValues.SepWriter ToFile(this nietras.SeparatedValues.SepWriterOptions options, string filePath) { }
public static nietras.SeparatedValues.SepWriter ToText(this nietras.SeparatedValues.SepWriterOptions options) { }
public static nietras.SeparatedValues.SepWriter ToText(this nietras.SeparatedValues.SepWriterOptions options, int capacity) { }
public static nietras.SeparatedValues.SepWriter To(in this nietras.SeparatedValues.SepWriterOptions options, System.IO.Stream stream) { }
public static nietras.SeparatedValues.SepWriter To(in this nietras.SeparatedValues.SepWriterOptions options, System.IO.TextWriter writer) { }
public static nietras.SeparatedValues.SepWriter To(in this nietras.SeparatedValues.SepWriterOptions options, System.Text.StringBuilder stringBuilder) { }
public static nietras.SeparatedValues.SepWriter To(in this nietras.SeparatedValues.SepWriterOptions options, System.IO.Stream stream, bool leaveOpen) { }
public static nietras.SeparatedValues.SepWriter To(in this nietras.SeparatedValues.SepWriterOptions options, System.IO.TextWriter writer, bool leaveOpen) { }
public static nietras.SeparatedValues.SepWriter To(in this nietras.SeparatedValues.SepWriterOptions options, string name, System.Func<string, System.IO.Stream> nameToStream, bool leaveOpen = false) { }
public static nietras.SeparatedValues.SepWriter To(in this nietras.SeparatedValues.SepWriterOptions options, string name, System.Func<string, System.IO.TextWriter> nameToWriter, bool leaveOpen = false) { }
public static nietras.SeparatedValues.SepWriter ToFile(in this nietras.SeparatedValues.SepWriterOptions options, string filePath) { }
public static nietras.SeparatedValues.SepWriter ToText(in this nietras.SeparatedValues.SepWriterOptions options) { }
public static nietras.SeparatedValues.SepWriter ToText(in this nietras.SeparatedValues.SepWriterOptions options, int capacity) { }
public static nietras.SeparatedValues.SepWriterOptions Writer(this nietras.SeparatedValues.Sep sep) { }
public static nietras.SeparatedValues.SepWriterOptions Writer(this nietras.SeparatedValues.SepSpec spec) { }
public static nietras.SeparatedValues.SepWriterOptions Writer(this nietras.SeparatedValues.Sep sep, System.Func<nietras.SeparatedValues.SepWriterOptions, nietras.SeparatedValues.SepWriterOptions> configure) { }
Expand Down
3 changes: 1 addition & 2 deletions .codecov.yml → codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ coverage:
project: true
patch: false
changes: false
codecov:
notify:
after_n_builds: 6
comment:
after_n_builds: 6
110 changes: 110 additions & 0 deletions src/Sep.Test/SepWriterTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -766,6 +766,116 @@ public void SepWriterTest_DisableColCountCheck_ColNotSetEmpty_NoHeader()
Assert.AreEqual(expected, writer.ToString());
}

[TestMethod]
public void SepWriterTest_DebuggerDisplay_ToText()
{
using var writer = Sep.Writer().ToText();
Assert.AreEqual("StringBuilder Length=0", writer.DebuggerDisplay);
WriteForDebuggerDisplay(writer);
Assert.AreEqual($"StringBuilder Length={WriteForDebuggerDisplayLength}", writer.DebuggerDisplay);
}

[TestMethod]
public void SepWriterTest_DebuggerDisplay_ToText_Capacity()
{
using var writer = Sep.Writer().ToText(capacity: 2048);
Assert.AreEqual("StringBuilder Length=0", writer.DebuggerDisplay);
WriteForDebuggerDisplay(writer);
Assert.AreEqual($"StringBuilder Length={WriteForDebuggerDisplayLength}", writer.DebuggerDisplay);
}

[TestMethod]
public void SepWriterTest_DebuggerDisplay_ToFile()
{
var filePath = Path.GetTempFileName();
{
using var writer = Sep.Writer().ToFile(filePath);
Assert.AreEqual($"File='{filePath}'", writer.DebuggerDisplay);
}
File.Delete(filePath);
}

[TestMethod]
public void SepWriterTest_DebuggerDisplay_To_StringBuilder()
{
var t = "test";
var sb = new StringBuilder(t);
using var writer = Sep.Writer().To(sb);
Assert.AreEqual($"StringBuilder Length={t.Length}", writer.DebuggerDisplay);
WriteForDebuggerDisplay(writer);
Assert.AreEqual($"StringBuilder Length={t.Length + WriteForDebuggerDisplayLength}", writer.DebuggerDisplay);
}

[TestMethod]
public void SepWriterTest_DebuggerDisplay_To_NameStream()
{
var name = "TEST";
var stream = new MemoryStream();
using var writer = Sep.Writer().To(name, n => stream);
Assert.AreEqual($"Stream='System.IO.MemoryStream' Name='TEST' Length={stream.Length}", writer.DebuggerDisplay);
WriteForDebuggerDisplay(writer);
Assert.AreEqual($"Stream='System.IO.MemoryStream' Name='TEST' Length={stream.Length}", writer.DebuggerDisplay);
}

[TestMethod]
public void SepWriterTest_DebuggerDisplay_To_Stream()
{
var stream = new MemoryStream();
using var writer = Sep.Writer().To(stream);
Assert.AreEqual($"Stream='System.IO.MemoryStream' Length={stream.Length}", writer.DebuggerDisplay);
WriteForDebuggerDisplay(writer);
Assert.AreEqual($"Stream='System.IO.MemoryStream' Length={stream.Length}", writer.DebuggerDisplay);
}

[TestMethod]
public void SepWriterTest_DebuggerDisplay_To_NameTextWriter()
{
var name = "TEST";
var textWriter = new StringWriter();
using var writer = Sep.Writer().To(name, n => textWriter);
Assert.AreEqual($"TextWriter='System.IO.StringWriter' Name='TEST'", writer.DebuggerDisplay);
}

[TestMethod]
public void SepWriterTest_DebuggerDisplay_To_TextWriter_StringWriter()
{
var textWriter = new StringWriter();
using var writer = Sep.Writer().To(textWriter);
Assert.AreEqual($"TextWriter='System.IO.StringWriter'", writer.DebuggerDisplay);
}

[TestMethod]
public void SepWriterTest_DebuggerDisplay_To_TextWriter_StreamWriter()
{
var textWriter = new StreamWriter(new MemoryStream());
using var writer = Sep.Writer().To(textWriter);
Assert.AreEqual($"TextWriter='System.IO.StreamWriter'", writer.DebuggerDisplay);
}

[TestMethod]
public void SepWriterTest_Info_Ctor()
{
var info = new SepWriter.Info("A", (i, w) => "B");
Assert.IsNotNull(info.Source);
Assert.IsNotNull(info.DebuggerDisplay);
}

[TestMethod]
public void SepWriterTest_Info_Props()
{
var info = new SepWriter.Info() { Source = "A", DebuggerDisplay = (i, w) => "B" };
Assert.IsNotNull(info.Source);
Assert.IsNotNull(info.DebuggerDisplay);
}

static readonly int WriteForDebuggerDisplayLength = 6 + 2 * Environment.NewLine.Length;
static void WriteForDebuggerDisplay(SepWriter writer)
{
using var row = writer.NewRow();
row["A"].Set("1");
row["B"].Set("2");
}

static SepWriter CreateWriter() =>
Sep.New(';').Writer().ToText();

Expand Down
2 changes: 1 addition & 1 deletion src/Sep/SepReaderExtensions.IO.Async.cs
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ public static async ValueTask<SepReader> FromAsync(this SepReaderOptions options
}

#if SYNC
internal static SepReader FromWithInfo(SepReader.Info info, SepReaderOptions options, TextReader reader)
internal static SepReader FromWithInfo(SepReader.Info info, in SepReaderOptions options, TextReader reader)
#else
internal static async ValueTask<SepReader> FromWithInfoAsync(SepReader.Info info, SepReaderOptions options, TextReader reader, CancellationToken cancellationToken)
#endif
Expand Down
2 changes: 1 addition & 1 deletion src/Sep/SepReaderExtensions.IO.Sync.cs
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ public static async ValueTask<SepReader> FromAsync(this SepReaderOptions options
}

#if SYNC
internal static SepReader FromWithInfo(SepReader.Info info, SepReaderOptions options, TextReader reader)
internal static SepReader FromWithInfo(SepReader.Info info, in SepReaderOptions options, TextReader reader)
#else
internal static async ValueTask<SepReader> FromWithInfoAsync(SepReader.Info info, SepReaderOptions options, TextReader reader, CancellationToken cancellationToken)
#endif
Expand Down
11 changes: 10 additions & 1 deletion src/Sep/SepWriter.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Runtime.CompilerServices;
Expand All @@ -8,10 +9,16 @@

namespace nietras.SeparatedValues;

[DebuggerDisplay("{DebuggerDisplay,nq}")]
public sealed partial class SepWriter : IDisposable
, IAsyncDisposable
{
internal delegate string DebuggerDisplayFunc(Info info, TextWriter writer);
internal readonly record struct Info(object Source, DebuggerDisplayFunc DebuggerDisplay);
internal string DebuggerDisplay => _info.DebuggerDisplay(_info, _writer);

const int DefaultCapacity = 16;
readonly Info _info;
readonly Sep _sep;
readonly CultureInfo? _cultureInfo;
internal readonly bool _writeHeader;
Expand Down Expand Up @@ -39,8 +46,10 @@ public sealed partial class SepWriter : IDisposable
CancellationToken _newRowCancellationToken = CancellationToken.None;
int _cacheIndex = 0;

internal SepWriter(SepWriterOptions options, TextWriter writer, ISepTextWriterDisposer textWriterDisposer)
internal SepWriter(Info info, in SepWriterOptions options,
TextWriter writer, ISepTextWriterDisposer textWriterDisposer)
{
_info = info;
_sep = options.Sep;
_cultureInfo = options.CultureInfo;
_writeHeader = options.WriteHeader;
Expand Down
66 changes: 51 additions & 15 deletions src/Sep/SepWriterExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Diagnostics.Contracts;
using System.IO;
using System.Text;
using static nietras.SeparatedValues.SepWriter;

namespace nietras.SeparatedValues;

Expand Down Expand Up @@ -33,42 +34,77 @@ public static SepWriterOptions Writer(this SepSpec spec, Func<SepWriterOptions,
return configure(Writer(spec));
}

public static SepWriter ToText(this SepWriterOptions options)
public static SepWriter ToText(this in SepWriterOptions options) =>
To(options, new StringBuilder());

public static SepWriter ToText(this in SepWriterOptions options, int capacity) =>
To(options, new StringBuilder(capacity));

public static SepWriter ToFile(this in SepWriterOptions options, string filePath)
{
var writer = new StringWriter();
return To(options, writer);
DebuggerDisplayFunc display = static (info, writer) => $"File='{info.Source}'";
var writer = new StreamWriter(filePath, s_streamWriterOptions);
return ToWithInfo(new(filePath, display), options, writer, leaveOpen: false);
}

public static SepWriter ToText(this SepWriterOptions options, int capacity)
public static SepWriter To(this in SepWriterOptions options, StringBuilder stringBuilder)
{
var writer = new StringWriter(new StringBuilder(capacity));
return To(options, writer);
DebuggerDisplayFunc display = static (info, writer) =>
$"{nameof(StringBuilder)} {nameof(StringBuilder.Length)}={((StringBuilder)info.Source).Length}";
var writer = new StringWriter(stringBuilder);
return ToWithInfo(new(stringBuilder, display), options, writer, leaveOpen: false);
}

public static SepWriter ToFile(this SepWriterOptions options, string filePath)
public static SepWriter To(this in SepWriterOptions options, string name, Func<string, Stream> nameToStream, bool leaveOpen = false)
{
var writer = new StreamWriter(filePath, s_streamWriterOptions);
return To(options, writer);
ArgumentNullException.ThrowIfNull(nameToStream);
DebuggerDisplayFunc display = static (info, writer) =>
{
var stream = ((StreamWriter)writer).BaseStream;
return $"{nameof(Stream)}='{stream}' Name='{info.Source}' Length={stream.Length}";
};
var stream = nameToStream(name);
var writer = new StreamWriter(stream, leaveOpen: leaveOpen);
// leaveOpen: false for StreamWriter is not the same as for Stream
return ToWithInfo(new(name, display), options, writer, leaveOpen: false);
}

public static SepWriter To(this SepWriterOptions options, Stream stream) =>
public static SepWriter To(this in SepWriterOptions options, Stream stream) =>
To(options, stream, leaveOpen: false);

public static SepWriter To(this SepWriterOptions options, Stream stream, bool leaveOpen)
public static SepWriter To(this in SepWriterOptions options, Stream stream, bool leaveOpen)
{
DebuggerDisplayFunc display = static (info, writer) => $"{nameof(Stream)}='{info.Source}' Length={((Stream)info.Source).Length}";
var writer = new StreamWriter(stream, leaveOpen: leaveOpen);
return To(options, writer);
// leaveOpen: false for StreamWriter is not the same as for Stream
return ToWithInfo(new(stream, display), options, writer, leaveOpen: false);
}

public static SepWriter To(this SepWriterOptions options, TextWriter writer) =>
public static SepWriter To(this in SepWriterOptions options, string name, Func<string, TextWriter> nameToWriter, bool leaveOpen = false)
{
ArgumentNullException.ThrowIfNull(nameToWriter);
DebuggerDisplayFunc display = static (info, writer) => $"{nameof(TextWriter)}='{writer.GetType()}' Name='{info.Source}'";
var writer = nameToWriter(name);
return ToWithInfo(new(name, display), options, writer, leaveOpen: false);
}

public static SepWriter To(this in SepWriterOptions options, TextWriter writer) =>
To(options, writer, leaveOpen: false);

public static SepWriter To(this SepWriterOptions options, TextWriter writer, bool leaveOpen)
public static SepWriter To(this in SepWriterOptions options, TextWriter writer, bool leaveOpen)
{
// Show type only to avoid calling ToString() on StringWriter (all contents)
DebuggerDisplayFunc display = static (info, writer) => $"{nameof(TextWriter)}='{info.Source.GetType()}'";
return ToWithInfo(new(writer, display), options, writer, leaveOpen);
}

static SepWriter ToWithInfo(SepWriter.Info info, in SepWriterOptions options,
TextWriter writer, bool leaveOpen)
{
ISepTextWriterDisposer textWriterDisposer = leaveOpen
? NoopSepTextWriterDisposer.Instance
: SepTextWriterDisposer.Instance;
ArgumentNullException.ThrowIfNull(writer);
return new SepWriter(options, writer, textWriterDisposer);
return new SepWriter(info, options, writer, textWriterDisposer);
}
}
2 changes: 2 additions & 0 deletions validate-codecov.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/bin/bash
curl -X POST --data-binary @codecov.yml https://codecov.io/validate

0 comments on commit eafedf6

Please sign in to comment.