-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Support for background services [ci]
- Loading branch information
Showing
13 changed files
with
532 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 1,77 @@ | ||
using System.Diagnostics.CodeAnalysis; | ||
using Microsoft.Extensions.DependencyInjection; | ||
using Microsoft.Extensions.Hosting; | ||
using Microsoft.Extensions.Logging; | ||
|
||
namespace BunsenBurner.Background.Tests; | ||
|
||
using static Aaa; | ||
using static BunsenBurner.Aaa; | ||
|
||
internal sealed class Background : BackgroundService | ||
{ | ||
private readonly ILogger<Background> _logger; | ||
|
||
public Background(ILogger<Background> logger) => _logger = logger; | ||
|
||
[ExcludeFromCodeCoverage] | ||
protected override async Task ExecuteAsync(CancellationToken stoppingToken) | ||
{ | ||
_logger.LogInformation("Starting work..."); | ||
while (!stoppingToken.IsCancellationRequested) | ||
{ | ||
var delay = TimeSpan.FromMilliseconds(10); | ||
_logger.LogInformation("Doing work for {Delay} duration", delay); | ||
await Task.Delay(delay, stoppingToken); | ||
_logger.LogInformation("Work complete"); | ||
} | ||
} | ||
} | ||
|
||
internal sealed class Startup | ||
{ | ||
public void ConfigureServices(IServiceCollection services) | ||
{ | ||
services.AddLogging(); | ||
services.AddHostedService<Background>(); | ||
} | ||
} | ||
|
||
public static class AaaTests | ||
{ | ||
[Fact(DisplayName = "A background service can be started and run for a period")] | ||
public static async Task Case1() => | ||
await ArrangeBackgroundService<Startup, Background>() | ||
.ActAndRunFor(TimeSpan.FromMilliseconds(40)) | ||
.Assert(store => | ||
{ | ||
Assert.NotEmpty(store); | ||
Assert.Contains(store, x => x.Message == "Work complete"); | ||
}); | ||
|
||
[Fact( | ||
DisplayName = "A background service can be started and run for a period with a description" | ||
)] | ||
public static async Task Case2() => | ||
await "Some description" | ||
.ArrangeBackgroundService<Startup, Background>() | ||
.ActAndRunFor(TimeSpan.FromMilliseconds(40)) | ||
.Assert(store => | ||
{ | ||
Assert.NotEmpty(store); | ||
Assert.Contains(store, x => x.Message == "Work complete"); | ||
}); | ||
|
||
[Fact( | ||
DisplayName = "A background service can be started and run for a period with existing arranged data" | ||
)] | ||
public static async Task Case3() => | ||
await Arrange(() => 1) | ||
.AndABackgroundService<int, Startup, Background>() | ||
.ActAndRunFor(x => x.BackgroundServiceContext, TimeSpan.FromMilliseconds(40)) | ||
.Assert(store => | ||
{ | ||
Assert.NotEmpty(store); | ||
Assert.Contains(store, x => x.Message == "Work complete"); | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 1,43 @@ | ||
namespace BunsenBurner.Background.Tests; | ||
|
||
using static Bdd; | ||
using static BunsenBurner.Bdd; | ||
|
||
public static class BddTests | ||
{ | ||
[Fact(DisplayName = "A background service can be started and run for a period")] | ||
public static async Task Case1() => | ||
await GivenABackgroundService<Startup, Background>() | ||
.WhenRunFor(TimeSpan.FromMilliseconds(40)) | ||
.Then(store => | ||
{ | ||
Assert.NotEmpty(store); | ||
Assert.Contains(store, x => x.Message == "Work complete"); | ||
}); | ||
|
||
[Fact( | ||
DisplayName = "A background service can be started and run for a period with a description" | ||
)] | ||
public static async Task Case2() => | ||
await "Some description" | ||
.GivenABackgroundService<Startup, Background>() | ||
.WhenRunFor(TimeSpan.FromMilliseconds(40)) | ||
.Then(store => | ||
{ | ||
Assert.NotEmpty(store); | ||
Assert.Contains(store, x => x.Message == "Work complete"); | ||
}); | ||
|
||
[Fact( | ||
DisplayName = "A background service can be started and run for a period with existing arranged data" | ||
)] | ||
public static async Task Case3() => | ||
await Given(() => 1) | ||
.AndABackgroundService<int, Startup, Background>() | ||
.WhenRunFor(x => x.BackgroundServiceContext, TimeSpan.FromMilliseconds(40)) | ||
.Then(store => | ||
{ | ||
Assert.NotEmpty(store); | ||
Assert.Contains(store, x => x.Message == "Work complete"); | ||
}); | ||
} |
28 changes: 28 additions & 0 deletions
28
BunsenBurner.Background.Tests/BunsenBurner.Background.Tests.csproj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 1,28 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<TargetFramework>net6.0</TargetFramework> | ||
<ImplicitUsings>enable</ImplicitUsings> | ||
<Nullable>enable</Nullable> | ||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors> | ||
<IsPackable>false</IsPackable> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" /> | ||
<PackageReference Include="xunit" Version="2.4.1" /> | ||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3"> | ||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||
<PrivateAssets>all</PrivateAssets> | ||
</PackageReference> | ||
<PackageReference Include="coverlet.collector" Version="3.1.2"> | ||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||
<PrivateAssets>all</PrivateAssets> | ||
</PackageReference> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<ProjectReference Include="..\BunsenBurner.Background\BunsenBurner.Background.csproj" /> | ||
</ItemGroup> | ||
|
||
</Project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 1 @@ | ||
global using Xunit; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 1,93 @@ | ||
using BunsenBurner.Logging; | ||
using Microsoft.Extensions.Hosting; | ||
|
||
namespace BunsenBurner.Background; | ||
|
||
using AaaScenario = Scenario<Syntax.Aaa>; | ||
|
||
/// <summary> | ||
/// DSL for building tests using an arrange, act, assert syntax | ||
/// </summary> | ||
public static class Aaa | ||
{ | ||
/// <summary> | ||
/// Arranges a background service context to run | ||
/// </summary> | ||
/// <typeparam name="TStartup">startup class</typeparam> | ||
/// <typeparam name="TBackgroundService">background service class</typeparam> | ||
/// <returns>arranged scenario</returns> | ||
[Pure] | ||
public static AaaScenario.Arranged< | ||
BackgroundServiceContext<TBackgroundService> | ||
> ArrangeBackgroundService<TStartup, TBackgroundService>() | ||
where TBackgroundService : IHostedService | ||
where TStartup : new() => | ||
Shared.ArrangeBackgroundService<TStartup, TBackgroundService, Syntax.Aaa>(); | ||
|
||
/// <summary> | ||
/// Arranges a background service context to run | ||
/// </summary> | ||
/// <param name="name">name/description</param> | ||
/// <typeparam name="TStartup">startup class</typeparam> | ||
/// <typeparam name="TBackgroundService">background service class</typeparam> | ||
/// <returns>arranged scenario</returns> | ||
[Pure] | ||
public static AaaScenario.Arranged< | ||
BackgroundServiceContext<TBackgroundService> | ||
> ArrangeBackgroundService<TStartup, TBackgroundService>(this string name) | ||
where TBackgroundService : IHostedService | ||
where TStartup : new() => | ||
name.ArrangeBackgroundService<TStartup, TBackgroundService, Syntax.Aaa>(); | ||
|
||
/// <summary> | ||
/// Arranges a background service context to run along with existing arranged data | ||
/// </summary> | ||
/// <param name="scenario">scenario</param> | ||
/// <typeparam name="TData">current arranged data</typeparam> | ||
/// <typeparam name="TStartup">startup class</typeparam> | ||
/// <typeparam name="TBackgroundService">background service class</typeparam> | ||
/// <returns>arranged scenario</returns> | ||
[Pure] | ||
public static AaaScenario.Arranged<(TData Data, BackgroundServiceContext<TBackgroundService> BackgroundServiceContext)> AndABackgroundService< | ||
TData, | ||
TStartup, | ||
TBackgroundService | ||
>(this AaaScenario.Arranged<TData> scenario) | ||
where TBackgroundService : IHostedService | ||
where TStartup : new() => | ||
scenario.AndABackgroundService<TData, TStartup, TBackgroundService, Syntax.Aaa>(); | ||
|
||
/// <summary> | ||
/// Runs the background service for the given time, returning any log messages | ||
/// </summary> | ||
/// <param name="scenario">arranged scenario</param> | ||
/// <param name="fn">function to setup and return the background service and store</param> | ||
/// <param name="runDuration">duration the background service should run for</param> | ||
/// <typeparam name="TData">arranged data</typeparam> | ||
/// <typeparam name="TBackgroundService">background service to test</typeparam> | ||
/// <returns>acted scenario</returns> | ||
[Pure] | ||
public static AaaScenario.Acted<TData, LogMessageStore> ActAndRunFor<TData, TBackgroundService>( | ||
this AaaScenario.Arranged<TData> scenario, | ||
Func<TData, BackgroundServiceContext<TBackgroundService>> fn, | ||
TimeSpan runDuration | ||
) where TBackgroundService : IHostedService => | ||
scenario.ActAndRunFor<TData, TBackgroundService, Syntax.Aaa>(fn, runDuration); | ||
|
||
/// <summary> | ||
/// Runs the background service for the given time, returning any log messages | ||
/// </summary> | ||
/// <param name="scenario">arranged scenario</param> | ||
/// <param name="runDuration">duration the background service should run for</param> | ||
/// <typeparam name="TBackgroundService">background service to test</typeparam> | ||
/// <returns>acted scenario</returns> | ||
[Pure] | ||
public static AaaScenario.Acted< | ||
BackgroundServiceContext<TBackgroundService>, | ||
LogMessageStore | ||
> ActAndRunFor<TBackgroundService>( | ||
this AaaScenario.Arranged<BackgroundServiceContext<TBackgroundService>> scenario, | ||
TimeSpan runDuration | ||
) where TBackgroundService : IHostedService => | ||
scenario.ActAndRunFor<TBackgroundService, Syntax.Aaa>(runDuration); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 1,56 @@ | ||
using System.Collections.Concurrent; | ||
using BunsenBurner.Logging; | ||
using Microsoft.Extensions.DependencyInjection; | ||
using Microsoft.Extensions.Hosting; | ||
|
||
namespace BunsenBurner.Background; | ||
|
||
/// <summary> | ||
/// Provides a cache/builder for background services | ||
/// </summary> | ||
public static class BackgroundServiceBuilder | ||
{ | ||
private static ConcurrentDictionary<string, Lazy<IServiceProvider>> ServiceProviderCache => | ||
new(); | ||
|
||
/// <summary> | ||
/// Creates a new instance of the background service and log message store from the provided startup class | ||
/// </summary> | ||
/// <typeparam name="TStartup">startup class</typeparam> | ||
/// <typeparam name="TBackgroundService">background service</typeparam> | ||
/// <returns>instance of the background service and log message store</returns> | ||
public static BackgroundServiceContext<TBackgroundService> Create< | ||
TStartup, | ||
TBackgroundService | ||
>() | ||
where TStartup : new() | ||
where TBackgroundService : IHostedService | ||
{ | ||
var startupType = typeof(TStartup); | ||
var sp = ServiceProviderCache | ||
.GetOrAdd( | ||
startupType.FullName ?? startupType.Name, | ||
static _ => | ||
new Lazy<IServiceProvider>(() => | ||
{ | ||
var store = LogMessageStore.New(); | ||
var configureServicesMethod = typeof(TStartup).GetMethod( | ||
"ConfigureServices" | ||
); | ||
var sc = new ServiceCollection(); | ||
var startup = new TStartup(); | ||
configureServicesMethod?.Invoke(startup, new object[] { sc }); | ||
sc.ClearLoggingProviders().AddDummyLogger(store).AddSingleton(store); | ||
return sc.BuildServiceProvider(); | ||
}) | ||
) | ||
.Value; | ||
var services = sp.GetRequiredService<IEnumerable<IHostedService>>(); | ||
var store = sp.GetRequiredService<LogMessageStore>(); | ||
store.Clear(); | ||
return new BackgroundServiceContext<TBackgroundService>( | ||
services.OfType<TBackgroundService>().First(), | ||
store | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 1,15 @@ | ||
using BunsenBurner.Logging; | ||
using Microsoft.Extensions.Hosting; | ||
|
||
namespace BunsenBurner.Background; | ||
|
||
/// <summary> | ||
/// Context used to test a background service | ||
/// </summary> | ||
/// <param name="Service">background service</param> | ||
/// <param name="Store">log message store</param> | ||
/// <typeparam name="TBackgroundService">background service type</typeparam> | ||
public sealed record BackgroundServiceContext<TBackgroundService>( | ||
TBackgroundService Service, | ||
LogMessageStore Store | ||
) where TBackgroundService : IHostedService; |
Oops, something went wrong.