Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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.

Copy link
Member

Choose a reason for hiding this comment

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

Sorry, I was not clear with this one. This "override" is not necessary. The method in the base class is implemented in the same way, I think. So delete this method?

{
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
122 changes: 117 additions & 5 deletions dotnet/Cucumber.HtmlFormatter/MessagesToHtmlWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,43 @@ 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;

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) { }

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 +52,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 +102,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 +157,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