Skip to content

Commit a1b3995

Browse files
authored
.net: Add async implementations to the HtmlFormatter. (#376)
1 parent 99216f8 commit a1b3995

File tree

5 files changed

+374
-5
lines changed

5 files changed

+374
-5
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
66
and this project adheres to [Semantic Versioning](http://semver.org/).
77

88
## [Unreleased]
9+
### Added
10+
- [.Net] Added asynchronous implementations of the HtmlFormatter methods ([#376](https://github.com/cucumber/html-formatter/pull/376))
11+
12+
### Deprecated
13+
- [.Net] Synchronous implementations of the HtmlFormatter methods ([#376](https://github.com/cucumber/html-formatter/pull/376))
914

1015
## [21.11.0] - 2025-05-25
1116
### Changed

dotnet/Cucumber.HtmlFormatter/JsonInHtmlWriter.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ public override void Write(string value)
2424
Write(value.ToCharArray(), 0, value.Length);
2525
}
2626

27+
public override async Task WriteAsync(string value)
28+
{
29+
await WriteAsync(value.ToCharArray(), 0, value.Length);
30+
}
31+
2732
public override void Write(char[] value)
2833
{
2934
Write(value, 0, value.GetLength(0));
@@ -62,6 +67,38 @@ public override void Write(char[] source, int offset, int length)
6267
}
6368
}
6469

70+
public override async Task WriteAsync(char[] source, int offset, int length)
71+
{
72+
if (offset + length > source.GetLength(0))
73+
throw new ArgumentException("Cannot read past the end of the input source char array.");
74+
75+
char[] destination = PrepareBuffer();
76+
int flushAt = BUFFER_SIZE - 2;
77+
int written = 0;
78+
for (int i = offset; i < offset + length; i++)
79+
{
80+
char c = source[i];
81+
82+
// Flush buffer if (nearly) full
83+
if (written >= flushAt)
84+
{
85+
await Writer.WriteAsync(destination, 0, written);
86+
written = 0;
87+
}
88+
89+
// Write with escapes
90+
if (c == '/')
91+
{
92+
destination[written++] = '\\';
93+
}
94+
destination[written++] = c;
95+
}
96+
// Flush any remaining
97+
if (written > 0)
98+
{
99+
await Writer.WriteAsync(destination, 0, written);
100+
}
101+
}
65102
private char[] PrepareBuffer()
66103
{
67104
// Reuse the same buffer, avoids repeated array allocation
@@ -74,6 +111,11 @@ public override void Flush()
74111
Writer.Flush();
75112
}
76113

114+
public override async Task FlushAsync()
115+
{
116+
await Writer.FlushAsync();
117+
}
118+
77119
public override void Close()
78120
{
79121
Writer.Close();

dotnet/Cucumber.HtmlFormatter/MessagesToHtmlWriter.cs

Lines changed: 120 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,46 @@ namespace Cucumber.HtmlFormatter;
66
public class MessagesToHtmlWriter : IDisposable
77
{
88
private StreamWriter writer;
9+
private Func<StreamWriter, Envelope, Task> asyncStreamSerializer;
910
private Action<StreamWriter, Envelope> streamSerializer;
1011
private string template;
1112
private JsonInHtmlWriter JsonInHtmlWriter;
1213
private bool streamClosed = false;
1314
private bool preMessageWritten = false;
1415
private bool firstMessageWritten = false;
1516
private bool postMessageWritten = false;
17+
private bool isAsyncInitialized = false;
1618

19+
[Obsolete("Cucumber.HtmlFormatter moving to async only operations. Please use the MessagesToHtmlWriter(Stream, Func<StreamWriter, Envelope, Task>) constructor", false)]
1720
public MessagesToHtmlWriter(Stream stream, Action<StreamWriter, Envelope> streamSerializer) : this(new StreamWriter(stream), streamSerializer)
1821
{
1922
}
23+
public MessagesToHtmlWriter(Stream stream, Func<StreamWriter, Envelope, Task> asyncStreamSerializer) : this(new StreamWriter(stream), asyncStreamSerializer) { }
24+
25+
[Obsolete("Cucumber.HtmlFormatter moving to async only operations. Please use the MessagesToHtmlWriter(StreamWriter, Func<StreamWriter, Envelope, Task>) constructor", false)]
2026
public MessagesToHtmlWriter(StreamWriter writer, Action<StreamWriter, Envelope> streamSerializer)
2127
{
2228
this.writer = writer;
2329
this.streamSerializer = streamSerializer;
30+
// Create async wrapper for sync serializer
31+
this.asyncStreamSerializer = (w, e) =>
32+
{
33+
streamSerializer(w, e);
34+
return Task.CompletedTask;
35+
};
2436
template = GetResource("index.mustache.html");
2537
JsonInHtmlWriter = new JsonInHtmlWriter(writer);
38+
isAsyncInitialized = false;
39+
}
40+
public MessagesToHtmlWriter(StreamWriter writer, Func<StreamWriter, Envelope, Task> asyncStreamSerializer)
41+
{
42+
this.writer = writer;
43+
this.asyncStreamSerializer = asyncStreamSerializer;
44+
// Create sync wrapper for async serializer (will block)
45+
this.streamSerializer = (w, e) => asyncStreamSerializer(w, e).GetAwaiter().GetResult();
46+
template = GetResource("index.mustache.html");
47+
JsonInHtmlWriter = new JsonInHtmlWriter(writer);
48+
isAsyncInitialized = true;
2649
}
2750

2851
private void WritePreMessage()
@@ -32,15 +55,35 @@ private void WritePreMessage()
3255
WriteTemplateBetween(writer, template, "{{css}}", "{{messages}}");
3356
}
3457

58+
private async Task WritePreMessageAsync()
59+
{
60+
await WriteTemplateBetweenAsync(writer, template, null, "{{css}}");
61+
await WriteResourceAsync(writer, "main.css");
62+
await WriteTemplateBetweenAsync(writer, template, "{{css}}", "{{messages}}");
63+
}
64+
3565
private void WritePostMessage()
3666
{
3767
WriteTemplateBetween(writer, template, "{{messages}}", "{{script}}");
3868
WriteResource(writer, "main.js");
3969
WriteTemplateBetween(writer, template, "{{script}}", null);
4070
}
4171

72+
private async Task WritePostMessageAsync()
73+
{
74+
await WriteTemplateBetweenAsync(writer, template, "{{messages}}", "{{script}}");
75+
await WriteResourceAsync(writer, "main.js");
76+
await WriteTemplateBetweenAsync(writer, template, "{{script}}", null);
77+
}
78+
4279
public void Write(Envelope envelope)
4380
{
81+
if (isAsyncInitialized)
82+
{
83+
// Log a warning or use other diagnostics
84+
System.Diagnostics.Debug.WriteLine("Warning: Using synchronous Write when initialized with async serializer");
85+
}
86+
4487
if (streamClosed) { throw new IOException("Stream closed"); }
4588

4689
if (!preMessageWritten)
@@ -62,6 +105,37 @@ public void Write(Envelope envelope)
62105
streamSerializer(JsonInHtmlWriter, envelope);
63106
JsonInHtmlWriter.Flush();
64107
}
108+
public async Task WriteAsync(Envelope envelope)
109+
{
110+
if (!isAsyncInitialized)
111+
{
112+
// Log a warning or use other diagnostics
113+
System.Diagnostics.Debug.WriteLine("Warning: Using asynchronous WriteAsync when initialized with sync serializer");
114+
}
115+
116+
if (streamClosed) { throw new IOException("Stream closed"); }
117+
118+
if (!preMessageWritten)
119+
{
120+
await WritePreMessageAsync();
121+
preMessageWritten = true;
122+
await writer.FlushAsync();
123+
}
124+
if (!firstMessageWritten)
125+
{
126+
firstMessageWritten = true;
127+
}
128+
else
129+
{
130+
await writer.WriteAsync(",");
131+
await writer.FlushAsync();
132+
}
133+
134+
// Use the synchronous serializer in an async context
135+
await asyncStreamSerializer(JsonInHtmlWriter, envelope);
136+
await JsonInHtmlWriter.FlushAsync();
137+
}
138+
65139
public void Dispose()
66140
{
67141
if (streamClosed) { return; }
@@ -86,27 +160,68 @@ public void Dispose()
86160
streamClosed = true;
87161
}
88162
}
163+
164+
public async Task DisposeAsync()
165+
{
166+
if (streamClosed) { return; }
167+
168+
if (!preMessageWritten)
169+
{
170+
await WritePreMessageAsync();
171+
preMessageWritten = true;
172+
}
173+
if (!postMessageWritten)
174+
{
175+
await WritePostMessageAsync();
176+
postMessageWritten = true;
177+
}
178+
try
179+
{
180+
await writer.FlushAsync();
181+
writer.Close();
182+
}
183+
finally
184+
{
185+
streamClosed = true;
186+
}
187+
}
188+
89189
private void WriteResource(StreamWriter writer, string v)
90190
{
91191
var resource = GetResource(v);
92192
writer.Write(resource);
93193
}
94194

195+
private async Task WriteResourceAsync(StreamWriter writer, string v)
196+
{
197+
var resource = GetResource(v);
198+
await writer.WriteAsync(resource);
199+
}
95200
private void WriteTemplateBetween(StreamWriter writer, string template, string? begin, string? end)
96201
{
97-
int beginIndex = begin == null ? 0 : template.IndexOf(begin) + begin.Length;
98-
int endIndex = end == null ? template.Length : template.IndexOf(end);
99-
int lengthToWrite = endIndex - beginIndex;
202+
int beginIndex, lengthToWrite;
203+
CalculateBeginAndLength(template, begin, end, out beginIndex, out lengthToWrite);
100204
writer.Write(template.Substring(beginIndex, lengthToWrite));
101205
}
102206

207+
private static void CalculateBeginAndLength(string template, string? begin, string? end, out int beginIndex, out int lengthToWrite)
208+
{
209+
beginIndex = begin == null ? 0 : template.IndexOf(begin) + begin.Length;
210+
int endIndex = end == null ? template.Length : template.IndexOf(end);
211+
lengthToWrite = endIndex - beginIndex;
212+
}
213+
214+
private async Task WriteTemplateBetweenAsync(StreamWriter writer, string template, string? begin, string? end)
215+
{
216+
int beginIndex, lengthToWrite;
217+
CalculateBeginAndLength(template, begin, end, out beginIndex, out lengthToWrite);
218+
await writer.WriteAsync(template.Substring(beginIndex, lengthToWrite));
219+
}
103220
private string GetResource(string name)
104221
{
105222
var assembly = typeof(MessagesToHtmlWriter).Assembly;
106223
var resourceStream = assembly.GetManifestResourceStream("Cucumber.HtmlFormatter.Resources." + name);
107224
var resource = new StreamReader(resourceStream).ReadToEnd();
108225
return resource;
109226
}
110-
111-
112227
}

0 commit comments

Comments
 (0)