Skip to content

Commit

Permalink
Add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
ltrzesniewski committed Apr 5, 2024
1 parent 7f50d7a commit ba94f7a
Show file tree
Hide file tree
Showing 5 changed files with 175 additions and 5 deletions.
2 changes: 1 addition & 1 deletion src/RazorBlade.Library/RazorTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ private protected virtual async Task<IRazorExecutionResult> ExecuteAsyncCore(Tex
/// <remarks>
/// This feature is not compatible with layouts.
/// </remarks>
protected Task FlushAsync()
protected internal Task FlushAsync()
=> _executionScope?.FlushAsync() ?? Task.CompletedTask;

/// <summary>
Expand Down
47 changes: 43 additions & 4 deletions src/RazorBlade.Tests/HtmlLayoutTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -184,12 +184,51 @@ public void should_throw_on_render()
Assert.Throws<InvalidOperationException>(() => ((RazorTemplate)layout).Render(CancellationToken.None));
}

private class Template(Action<Template> executeAction) : HtmlTemplate
[Test]
public void should_throw_when_setting_layout_after_flush()
{
protected internal override Task ExecuteAsync()
var layout = new Layout(_ => { });

var page = new Template(async t =>
{
executeAction(this);
return base.ExecuteAsync();
await t.FlushAsync();
t.Layout = layout;
});

Assert.Throws<InvalidOperationException>(() => page.Render())
.ShouldNotBeNull().Message.ShouldEqual("The layout can no longer be changed.");
}

[Test]
public void should_throw_when_flushing_with_layout()
{
var layout = new Layout(_ => { });

var page = new Template(async t =>
{
t.Layout = layout;
await t.FlushAsync();
});

Assert.Throws<InvalidOperationException>(() => page.Render())
.ShouldNotBeNull().Message.ShouldEqual("The output cannot be flushed when a layout is used.");
}

private class Template(Func<Template, Task> executeAction) : HtmlTemplate
{
public Template(Action<Template> executeAction)
: this(t =>
{
executeAction(t);
return Task.CompletedTask;
})
{
}

protected internal override async Task ExecuteAsync()
{
await executeAction(this);
await base.ExecuteAsync();
}

public void SetSection(string name, string content)
Expand Down
85 changes: 85 additions & 0 deletions src/RazorBlade.Tests/RazorTemplateTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,91 @@ public void should_not_execute_template_when_already_cancelled()
semaphore.CurrentCount.ShouldEqual(0);
}

[Test]
public async Task should_flush_output()
{
var output = new StringWriter();
var stepSemaphore = new StepSemaphore();

var template = new Template(async t =>
{
await stepSemaphore.WaitForNextStepAsync();

t.WriteLiteral("foo");
await t.FlushAsync();

await stepSemaphore.WaitForNextStepAsync();

t.WriteLiteral(" bar");
await t.FlushAsync();

await stepSemaphore.WaitForNextStepAsync();

t.WriteLiteral(" baz");
await t.FlushAsync();

await stepSemaphore.NotifyEndOfLastStepAsync();
});

var task = template.RenderAsync(output);

await stepSemaphore.StartNextStepAndWaitForResultAsync();
output.ToString().ShouldEqual("foo");
template.Output.ShouldBe<StringWriter>().ToString().ShouldEqual(string.Empty);

await stepSemaphore.StartNextStepAndWaitForResultAsync();
output.ToString().ShouldEqual("foo bar");
template.Output.ShouldBe<StringWriter>().ToString().ShouldEqual(string.Empty);

await stepSemaphore.StartNextStepAndWaitForResultAsync();
output.ToString().ShouldEqual("foo bar baz");

await task;
output.ToString().ShouldEqual("foo bar baz");
template.Output.ShouldEqual(TextWriter.Null);
}

[Test]
public async Task should_buffer_output_until_flushed()
{
var output = new StringWriter();
var stepSemaphore = new StepSemaphore();

var template = new Template(async t =>
{
await stepSemaphore.WaitForNextStepAsync();

t.WriteLiteral("foo");
await t.Output.FlushAsync();

await stepSemaphore.WaitForNextStepAsync();

t.WriteLiteral(" bar");
await t.Output.FlushAsync();

await stepSemaphore.WaitForNextStepAsync();

t.WriteLiteral(" baz");
await t.Output.FlushAsync();

await stepSemaphore.NotifyEndOfLastStepAsync();
});

var task = template.RenderAsync(output);

await stepSemaphore.StartNextStepAndWaitForResultAsync();
output.ToString().ShouldEqual(string.Empty);
template.Output.ShouldBe<StringWriter>().ToString().ShouldEqual("foo");

await stepSemaphore.StartNextStepAndWaitForResultAsync();
output.ToString().ShouldEqual(string.Empty);
template.Output.ShouldBe<StringWriter>().ToString().ShouldEqual("foo bar");

await stepSemaphore.StartNextStepAndWaitForResultAsync();
await task;
output.ToString().ShouldEqual("foo bar baz");
}

private class Template(Func<Template, Task> executeAction) : RazorTemplate
{
public Template(Action<Template> executeAction)
Expand Down
11 changes: 11 additions & 0 deletions src/RazorBlade.Tests/Support/AssertExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using JetBrains.Annotations;
using NUnit.Framework;

namespace RazorBlade.Tests.Support;

#if NET
[StackTraceHidden]
#endif
internal static class AssertExtensions
{
public static void ShouldEqual<T>(this T? actual, T? expected)
Expand All @@ -19,6 +23,13 @@ public static void ShouldBeTrue(this bool actual)
public static void ShouldBeFalse(this bool actual)
=> Assert.That(actual, Is.False);

public static TExpected ShouldBe<TExpected>(this object? actual)
where TExpected : class
{
Assert.That(actual, Is.InstanceOf<TExpected>());
return actual as TExpected ?? throw new AssertionException($"Expected instance of {typeof(TExpected).Name}");
}

[ContractAnnotation("notnull => halt")]
public static void ShouldBeNull(this object? actual)
=> Assert.That(actual, Is.Null);
Expand Down
35 changes: 35 additions & 0 deletions src/RazorBlade.Tests/Support/StepSemaphore.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System.Threading;
using System.Threading.Tasks;

namespace RazorBlade.Tests.Support;

public class StepSemaphore
{
private readonly SemaphoreSlim _semaphoreStartOfStep = new(0);
private readonly SemaphoreSlim _semaphoreEndOfStep = new(0);
private int _isFirstStep;

public async Task WaitForNextStepAsync()
{
if (Interlocked.Exchange(ref _isFirstStep, 1) != 0)
_semaphoreEndOfStep.Release();

var result = await _semaphoreStartOfStep.WaitAsync(10_000);
result.ShouldBeTrue();
}

public Task NotifyEndOfLastStepAsync()
{
_semaphoreEndOfStep.Release();

return Task.CompletedTask; // Just to make the API consistent
}

public async Task StartNextStepAndWaitForResultAsync()
{
_semaphoreStartOfStep.Release();

var result = await _semaphoreEndOfStep.WaitAsync(10_000);
result.ShouldBeTrue();
}
}

0 comments on commit ba94f7a

Please sign in to comment.