Skip to content
Merged
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]
### Added
Copy link
Contributor

Choose a reason for hiding this comment

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

I've changed this to added. It doesn't look like anything was removed. Is that correct w.r.t the guidance on http://semver.org?

- [.Net] Added asynchronous implementations of the HtmlFormatter methods ([#376](https://github.com/cucumber/html-formatter/pull/376))

### Deprecated
- [.Net] Synchronous implementations of the HtmlFormatter methods ([#376](https://github.com/cucumber/html-formatter/pull/376))

## [21.11.0] - 2025-05-25
### Changed
Expand Down
42 changes: 42 additions & 0 deletions dotnet/Cucumber.HtmlFormatter/JsonInHtmlWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ public override void Write(string value)
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));
Expand Down Expand Up @@ -62,6 +67,38 @@ public override void Write(char[] source, int offset, int length)
}
}

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 +111,11 @@ public override void Flush()
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)]
Copy link
Contributor

Choose a reason for hiding this comment

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

This looks like a deprecation. So this needs its own deprecated entry in the changelog. This will inform the decision for the next version number.

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