Skip to content

Added async implementations to the HtmlFormatter. #376

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]
### Changed
- Added asynchronous implementations of the HtmlFormatter methods in the .NET implementation

## [21.11.0] - 2025-05-25
### Changed
Expand Down
47 changes: 47 additions & 0 deletions dotnet/Cucumber.HtmlFormatter/JsonInHtmlWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,21 @@
Write(value.ToCharArray(), 0, value.Length);
}

public override async Task WriteAsync(string value)
{
await WriteAsync(value.ToCharArray(), 0, value.Length);
}

public override void Write(char[] value)
{
Write(value, 0, value.GetLength(0));
}

public async Task WriteAsync(char[] value)

Check warning on line 37 in dotnet/Cucumber.HtmlFormatter/JsonInHtmlWriter.cs

View workflow job for this annotation

GitHub Actions / test-dotnet

'JsonInHtmlWriter.WriteAsync(char[])' hides inherited member 'TextWriter.WriteAsync(char[])'. Use the new keyword if hiding was intended.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on the warning, this method should have been done anyway.

'JsonInHtmlWriter.WriteAsync(char[])' hides inherited member 'TextWriter.WriteAsync(char[])'. Use the new keyword if hiding was intended.

{
await WriteAsync(value, 0, value.GetLength(0));
}

public override void Write(char[] source, int offset, int length)
{
if (offset + length > source.GetLength(0))
Expand Down Expand Up @@ -62,6 +72,38 @@
}
}

public override async Task WriteAsync(char[] source, int offset, int length)
{
if (offset + length > source.GetLength(0))
throw new ArgumentException("Cannot read past the end of the input source char array.");

char[] destination = PrepareBuffer();
int flushAt = BUFFER_SIZE - 2;
int written = 0;
for (int i = offset; i < offset + length; i++)
{
char c = source[i];

// Flush buffer if (nearly) full
if (written >= flushAt)
{
await Writer.WriteAsync(destination, 0, written);
written = 0;
}

// Write with escapes
if (c == '/')
{
destination[written++] = '\\';
}
destination[written++] = c;
}
// Flush any remaining
if (written > 0)
{
await Writer.WriteAsync(destination, 0, written);
}
}
private char[] PrepareBuffer()
{
// Reuse the same buffer, avoids repeated array allocation
Expand All @@ -74,6 +116,11 @@
Writer.Flush();
}

public override async Task FlushAsync()
{
await Writer.FlushAsync();
}

public override void Close()
{
Writer.Close();
Expand Down
125 changes: 120 additions & 5 deletions dotnet/Cucumber.HtmlFormatter/MessagesToHtmlWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,46 @@ namespace Cucumber.HtmlFormatter;
public class MessagesToHtmlWriter : IDisposable
{
private StreamWriter writer;
private Func<StreamWriter, Envelope, Task> asyncStreamSerializer;
private Action<StreamWriter, Envelope> streamSerializer;
private string template;
private JsonInHtmlWriter JsonInHtmlWriter;
private bool streamClosed = false;
private bool preMessageWritten = false;
private bool firstMessageWritten = false;
private bool postMessageWritten = false;
private bool isAsyncInitialized = false;

[Obsolete("Cucumber.HtmlFormatter moving to async only operations. Please use the MessagesToHtmlWriter(Stream, Func<StreamWriter, Envelope, Task>) constructor", false)]
public MessagesToHtmlWriter(Stream stream, Action<StreamWriter, Envelope> streamSerializer) : this(new StreamWriter(stream), streamSerializer)
{
}
public MessagesToHtmlWriter(Stream stream, Func<StreamWriter, Envelope, Task> asyncStreamSerializer) : this(new StreamWriter(stream), asyncStreamSerializer) { }

[Obsolete("Cucumber.HtmlFormatter moving to async only operations. Please use the MessagesToHtmlWriter(StreamWriter, Func<StreamWriter, Envelope, Task>) constructor", false)]
public MessagesToHtmlWriter(StreamWriter writer, Action<StreamWriter, Envelope> streamSerializer)
{
this.writer = writer;
this.streamSerializer = streamSerializer;
// Create async wrapper for sync serializer
this.asyncStreamSerializer = (w, e) =>
{
streamSerializer(w, e);
return Task.CompletedTask;
};
template = GetResource("index.mustache.html");
JsonInHtmlWriter = new JsonInHtmlWriter(writer);
isAsyncInitialized = false;
}
public MessagesToHtmlWriter(StreamWriter writer, Func<StreamWriter, Envelope, Task> asyncStreamSerializer)
{
this.writer = writer;
this.asyncStreamSerializer = asyncStreamSerializer;
// Create sync wrapper for async serializer (will block)
this.streamSerializer = (w, e) => asyncStreamSerializer(w, e).GetAwaiter().GetResult();
template = GetResource("index.mustache.html");
JsonInHtmlWriter = new JsonInHtmlWriter(writer);
isAsyncInitialized = true;
}

private void WritePreMessage()
Expand All @@ -32,15 +55,35 @@ private void WritePreMessage()
WriteTemplateBetween(writer, template, "{{css}}", "{{messages}}");
}

private async Task WritePreMessageAsync()
{
await WriteTemplateBetweenAsync(writer, template, null, "{{css}}");
await WriteResourceAsync(writer, "main.css");
await WriteTemplateBetweenAsync(writer, template, "{{css}}", "{{messages}}");
}

private void WritePostMessage()
{
WriteTemplateBetween(writer, template, "{{messages}}", "{{script}}");
WriteResource(writer, "main.js");
WriteTemplateBetween(writer, template, "{{script}}", null);
}

private async Task WritePostMessageAsync()
{
await WriteTemplateBetweenAsync(writer, template, "{{messages}}", "{{script}}");
await WriteResourceAsync(writer, "main.js");
await WriteTemplateBetweenAsync(writer, template, "{{script}}", null);
}

public void Write(Envelope envelope)
{
if (isAsyncInitialized)
{
// Log a warning or use other diagnostics
System.Diagnostics.Debug.WriteLine("Warning: Using synchronous Write when initialized with async serializer");
}

if (streamClosed) { throw new IOException("Stream closed"); }

if (!preMessageWritten)
Expand All @@ -62,6 +105,37 @@ public void Write(Envelope envelope)
streamSerializer(JsonInHtmlWriter, envelope);
JsonInHtmlWriter.Flush();
}
public async Task WriteAsync(Envelope envelope)
{
if (!isAsyncInitialized)
{
// Log a warning or use other diagnostics
System.Diagnostics.Debug.WriteLine("Warning: Using asynchronous WriteAsync when initialized with sync serializer");
}

if (streamClosed) { throw new IOException("Stream closed"); }

if (!preMessageWritten)
{
await WritePreMessageAsync();
preMessageWritten = true;
await writer.FlushAsync();
}
if (!firstMessageWritten)
{
firstMessageWritten = true;
}
else
{
await writer.WriteAsync(",");
await writer.FlushAsync();
}

// Use the synchronous serializer in an async context
await asyncStreamSerializer(JsonInHtmlWriter, envelope);
await JsonInHtmlWriter.FlushAsync();
}

public void Dispose()
{
if (streamClosed) { return; }
Expand All @@ -86,27 +160,68 @@ public void Dispose()
streamClosed = true;
}
}

public async Task DisposeAsync()
{
if (streamClosed) { return; }

if (!preMessageWritten)
{
await WritePreMessageAsync();
preMessageWritten = true;
}
if (!postMessageWritten)
{
await WritePostMessageAsync();
postMessageWritten = true;
}
try
{
await writer.FlushAsync();
writer.Close();
}
finally
{
streamClosed = true;
}
}

private void WriteResource(StreamWriter writer, string v)
{
var resource = GetResource(v);
writer.Write(resource);
}

private async Task WriteResourceAsync(StreamWriter writer, string v)
{
var resource = GetResource(v);
await writer.WriteAsync(resource);
}
private void WriteTemplateBetween(StreamWriter writer, string template, string? begin, string? end)
{
int beginIndex = begin == null ? 0 : template.IndexOf(begin) + begin.Length;
int endIndex = end == null ? template.Length : template.IndexOf(end);
int lengthToWrite = endIndex - beginIndex;
int beginIndex, lengthToWrite;
CalculateBeginAndLength(template, begin, end, out beginIndex, out lengthToWrite);
writer.Write(template.Substring(beginIndex, lengthToWrite));
}

private static void CalculateBeginAndLength(string template, string? begin, string? end, out int beginIndex, out int lengthToWrite)
{
beginIndex = begin == null ? 0 : template.IndexOf(begin) + begin.Length;
int endIndex = end == null ? template.Length : template.IndexOf(end);
lengthToWrite = endIndex - beginIndex;
}

private async Task WriteTemplateBetweenAsync(StreamWriter writer, string template, string? begin, string? end)
{
int beginIndex, lengthToWrite;
CalculateBeginAndLength(template, begin, end, out beginIndex, out lengthToWrite);
await writer.WriteAsync(template.Substring(beginIndex, lengthToWrite));
}
private string GetResource(string name)
{
var assembly = typeof(MessagesToHtmlWriter).Assembly;
var resourceStream = assembly.GetManifestResourceStream("Cucumber.HtmlFormatter.Resources." + name);
var resource = new StreamReader(resourceStream).ReadToEnd();
return resource;
}


}
Loading