Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cosmos xUnit Fixtures #11

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
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
7 changes: 7 additions & 0 deletions Ephemerally.sln
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{EBD6
.github\workflows\tag.yml = .github\workflows\tag.yml
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ephemerally.Xunit", "src\Ephemerally.Xunit\Ephemerally.Xunit.csproj", "{BF2CF962-181D-4A63-B61E-32F3130B84DF}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -74,6 +76,10 @@ Global
{5A30E9EA-AB80-4E08-A81F-99CE329B34B8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5A30E9EA-AB80-4E08-A81F-99CE329B34B8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5A30E9EA-AB80-4E08-A81F-99CE329B34B8}.Release|Any CPU.Build.0 = Release|Any CPU
{BF2CF962-181D-4A63-B61E-32F3130B84DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BF2CF962-181D-4A63-B61E-32F3130B84DF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BF2CF962-181D-4A63-B61E-32F3130B84DF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BF2CF962-181D-4A63-B61E-32F3130B84DF}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -88,6 +94,7 @@ Global
{7E763132-0532-4689-A738-7CB71635AC63} = {F9102409-3748-4DA6-9D7B-784339508244}
{E186436A-12C9-4140-A279-19227CFF8533} = {AF30F58C-5E9B-4406-9E13-114C46F4E410}
{5A30E9EA-AB80-4E08-A81F-99CE329B34B8} = {F9102409-3748-4DA6-9D7B-784339508244}
{BF2CF962-181D-4A63-B61E-32F3130B84DF} = {F9102409-3748-4DA6-9D7B-784339508244}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5690E595-5302-46D7-AA86-89D55117B068}
Expand Down
8 changes: 8 additions & 0 deletions src/Ephemerally.Azure.Cosmos.Xunit/CosmosClientFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Microsoft.Azure.Cosmos;

namespace Ephemerally.Azure.Cosmos.Xunit;

public abstract class CosmosClientFixture : CosmosSubjectFixture<CosmosClient>
{
public CosmosClient Client => GetOrCreateSubjectAsync().Result;
}
24 changes: 24 additions & 0 deletions src/Ephemerally.Azure.Cosmos.Xunit/CosmosContainerFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using Ephemerally.Xunit;
using Microsoft.Azure.Cosmos;

namespace Ephemerally.Azure.Cosmos.Xunit;

public interface ICosmosContainerFixture<out TContainer>
: ISubjectFixture<Container>
where TContainer : Container
{
TContainer Container { get; }
}

public abstract class CosmosContainerFixture
: CosmosContainerFixture<Container>;

public abstract class CosmosContainerFixture<TContainer>
: CosmosSubjectFixture<TContainer>, ICosmosContainerFixture<TContainer>
where TContainer : Container
{
Task<Container> ISubjectFixture<Container>.GetOrCreateSubjectAsync() =>
GetOrCreateSubjectAsync().ContinueWith(Container (x) => x.Result);

public TContainer Container => GetOrCreateSubjectAsync().Result;
}
24 changes: 24 additions & 0 deletions src/Ephemerally.Azure.Cosmos.Xunit/CosmosDatabaseFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using Ephemerally.Xunit;
using Microsoft.Azure.Cosmos;

namespace Ephemerally.Azure.Cosmos.Xunit;

public interface ICosmosDatabaseFixture<out TDatabase>
: ISubjectFixture<Database>
where TDatabase : Database
{
TDatabase Database { get; }
}

public abstract class CosmosDatabaseFixture
: CosmosDatabaseFixture<Database>;

public abstract class CosmosDatabaseFixture<TDatabase>
: CosmosSubjectFixture<TDatabase>, ICosmosDatabaseFixture<TDatabase>
where TDatabase : Database
{
Task<Database> ISubjectFixture<Database>.GetOrCreateSubjectAsync() =>
GetOrCreateSubjectAsync().ContinueWith(Database (x) => x.Result);

public TDatabase Database => GetOrCreateSubjectAsync().Result;
}
27 changes: 27 additions & 0 deletions src/Ephemerally.Azure.Cosmos.Xunit/CosmosEmulator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Microsoft.Azure.Cosmos;

namespace Ephemerally.Azure.Cosmos.Xunit;

public static class CosmosEmulator
{
public static CosmosClient GetClient(Action<CosmosClientOptions> configureOptions = null)
{
var options = new CosmosClientOptions
{
RequestTimeout = TimeSpan.FromSeconds(30),
ServerCertificateCustomValidationCallback = (_, _, _) => true,
ConnectionMode = ConnectionMode.Gateway,
LimitToEndpoint = true
};
configureOptions?.Invoke(options);
return new(
AccountEndpoint,
AuthKey,
options);
}

public const string
AccountEndpoint = "https://localhost:8081",
AuthKey = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==",
ConnectionString = $"AccountEndpoint={AccountEndpoint};AccountKey={AuthKey};";
}
8 changes: 8 additions & 0 deletions src/Ephemerally.Azure.Cosmos.Xunit/CosmosSubjectFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Ephemerally.Xunit;

namespace Ephemerally.Azure.Cosmos.Xunit;

public abstract class CosmosSubjectFixture<TSubject> : SubjectFixture<TSubject> where TSubject : class
{
protected override Task DisposeSubjectAsync() => SafeCosmosDisposeAsync(GetOrCreateSubjectAsync);
}
43 changes: 43 additions & 0 deletions src/Ephemerally.Azure.Cosmos.Xunit/DefaultFixtures.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.Azure.Cosmos;

namespace Ephemerally.Azure.Cosmos.Xunit;

public class DefaultCosmosEmulatorClientFixture : CosmosClientFixture
{
protected override Task<CosmosClient> CreateSubjectAsync() => Task.FromResult(CosmosEmulator.GetClient());
}

[SuppressMessage("ReSharper", "UseConfigureAwaitFalse")]
public class DefaultEphemeralCosmosDatabaseFixture()
: EphemeralCosmosDatabaseFixture(new DefaultCosmosEmulatorClientFixture())
{
protected override async Task DisposeSubjectAsync()
{
try
{
await base.DisposeSubjectAsync();
}
finally
{
await CosmosClientFixture.DisposeAsync();
}
}
}

[SuppressMessage("ReSharper", "UseConfigureAwaitFalse")]
public class DefaultEphemeralCosmosContainerFixture()
: EphemeralCosmosContainerFixture(new DefaultEphemeralCosmosDatabaseFixture())
{
protected override async Task DisposeSubjectAsync()
{
try
{
await base.DisposeSubjectAsync();
}
finally
{
await CosmosDatabaseFixture.DisposeAsync();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,27 +1,19 @@
using System.Diagnostics.CodeAnalysis;
using Ephemerally.Xunit;
using Microsoft.Azure.Cosmos;
using Xunit;

namespace Ephemerally.Azure.Cosmos.Xunit;

internal abstract class EphemeralCosmosContainerFixture :
IAsyncDisposable,
IAsyncLifetime
[SuppressMessage("ReSharper", "UseConfigureAwaitFalse")]
public class EphemeralCosmosContainerFixture(ISubjectFixture<Database> cosmosDatabaseFixture)
: CosmosContainerFixture<EphemeralCosmosContainer>
{
private Lazy<ValueTask<EphemeralCosmosContainer>> _container;
// ReSharper disable once MemberCanBePrivate.Global
protected ISubjectFixture<Database> CosmosDatabaseFixture { get; } = cosmosDatabaseFixture;

protected EphemeralCosmosContainerFixture(
Database database,
EphemeralCreationOptions options)
protected override async Task<EphemeralCosmosContainer> CreateSubjectAsync()
{
_container = new(async () => await database.CreateEphemeralContainerAsync(options).ConfigureAwait(false));
var database = await CosmosDatabaseFixture.GetOrCreateSubjectAsync();
return await database.CreateEphemeralContainerAsync();
}

async Task IAsyncLifetime.InitializeAsync() =>
await _container.Value.ConfigureAwait(false);

async Task IAsyncLifetime.DisposeAsync() =>
await ((IAsyncDisposable)this).DisposeAsync().ConfigureAwait(false);

ValueTask IAsyncDisposable.DisposeAsync() =>
_container.Value.Result.DisposeAsync();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System.Diagnostics.CodeAnalysis;
using Ephemerally.Xunit;
using Microsoft.Azure.Cosmos;

namespace Ephemerally.Azure.Cosmos.Xunit;

[SuppressMessage("ReSharper", "UseConfigureAwaitFalse")]
public class EphemeralCosmosDatabaseFixture(ISubjectFixture<CosmosClient> cosmosClientFixture)
: CosmosDatabaseFixture<EphemeralCosmosDatabase>
{
// ReSharper disable once MemberCanBePrivate.Global
protected ISubjectFixture<CosmosClient> CosmosClientFixture { get; } = cosmosClientFixture;

protected override async Task<EphemeralCosmosDatabase> CreateSubjectAsync()
{
var client = await CosmosClientFixture.GetOrCreateSubjectAsync();
return await client.CreateEphemeralDatabaseAsync();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

<ItemGroup>
<ProjectReference Include="..\Ephemerally.Azure.Cosmos\Ephemerally.Azure.Cosmos.csproj" />
<ProjectReference Include="..\Ephemerally.Xunit\Ephemerally.Xunit.csproj" />
</ItemGroup>

</Project>
29 changes: 29 additions & 0 deletions src/Ephemerally.Azure.Cosmos.Xunit/Globals.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
global using static Ephemerally.Azure.Cosmos.Xunit.Globals;
using System.Diagnostics.CodeAnalysis;
using System.Net.Sockets;

namespace Ephemerally.Azure.Cosmos.Xunit;

[SuppressMessage("ReSharper", "UseConfigureAwaitFalse")]
internal static class Globals
{
public static Task SafeCosmosDisposeAsync<T>(Func<Task<T>> getT) where T : class =>
IgnoreSocketException(async () =>
{
var t = await getT();
// ReSharper disable once MethodHasAsyncOverload
if (!await t.TryDisposeAsync()) t.TryDispose();
});

public static async Task IgnoreSocketException(Func<Task> action)
{
try
{
await action();
}
catch (HttpRequestException ex) when (ex.InnerException is SocketException { SocketErrorCode: SocketError.ConnectionRefused })
{
// This can occur if the emulator or instance is not accessible; do nothing.
}
}
}
52 changes: 52 additions & 0 deletions src/Ephemerally.Xunit/DependentSubjectFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using System.Diagnostics.CodeAnalysis;

namespace Ephemerally.Xunit;

[SuppressMessage("ReSharper", "UseConfigureAwaitFalse")]
public class DependentSubjectFixture<TSubject> : ISubjectFixture<TSubject>
{
private readonly ISubjectFixture<TSubject> _implementation;
private readonly ISubjectFixture[] _dependencies;

public DependentSubjectFixture(
ISubjectFixture<TSubject> implementation,
params ISubjectFixture[] dependencies)
{
_implementation = implementation;
_dependencies = dependencies;
}

public Task<TSubject> GetOrCreateSubjectAsync() => _implementation.GetOrCreateSubjectAsync();

public Task InitializeAsync() => _implementation.InitializeAsync();

public async Task DisposeAsync()
{
try
{
await _implementation.DisposeAsync();
}
finally
{
var exceptions = new List<Exception>();
await Task.WhenAll(_dependencies.Select(async x =>
{
try
{
await x.DisposeAsync();
}
catch (Exception ex)
{
exceptions.Add(ex);
}
}
));
if (exceptions.Count > 0)
{
#pragma warning disable CA2219
throw new AggregateException(exceptions);
#pragma warning restore CA2219
}
}
}
}
16 changes: 16 additions & 0 deletions src/Ephemerally.Xunit/Ephemerally.Xunit.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>disable</Nullable>
<LangVersion>latest</LangVersion>
<IsTestProject>false</IsTestProject>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Xunit" Version="2.4.2" />
</ItemGroup>

</Project>
43 changes: 43 additions & 0 deletions src/Ephemerally.Xunit/SubjectFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System.Diagnostics.CodeAnalysis;
using Xunit;

namespace Ephemerally.Xunit;

public interface ISubjectFixture : IAsyncLifetime;

public interface ISubjectFixture<TSubject> : ISubjectFixture
{
Task<TSubject> GetOrCreateSubjectAsync();
}

[SuppressMessage("ReSharper", "UseConfigureAwaitFalse")]
public abstract class SubjectFixture<TSubject> :
ISubjectFixture<TSubject>,
IAsyncDisposable
{
private readonly Lazy<Task<TSubject>> _subject;

// Inheriting class should implement an accessor for the subject e.g.
// public TSubject Subject => _subject.Value.Result;

// ReSharper disable once MemberCanBePrivate.Global
public Task<TSubject> GetOrCreateSubjectAsync() => _subject.Value;

protected SubjectFixture() => _subject = new(CreateSubjectAsync);

protected abstract Task<TSubject> CreateSubjectAsync();

protected virtual Task DisposeSubjectAsync() => Task.CompletedTask;

public Task InitializeAsync() => _subject.Value;

public async Task DisposeAsync()
{
if (!_subject.IsValueCreated) return;

await DisposeSubjectAsync();
}

async ValueTask IAsyncDisposable.DisposeAsync() =>
await ((IAsyncLifetime)this).DisposeAsync();
}
1 change: 1 addition & 0 deletions src/Ephemerally/Ephemerally.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<ItemGroup>
<InternalsVisibleTo Include="$(AssemblyName).Tests" />
<InternalsVisibleTo Include="Ephemerally.Azure.Cosmos" />
<InternalsVisibleTo Include="Ephemerally.Azure.Cosmos.Xunit" />
<InternalsVisibleTo Include="Ephemerally.Redis" />
<InternalsVisibleTo Include="Ephemerally.Redis.Tests" />
<InternalsVisibleTo Include="Ephemerally.Redis.Xunit" />
Expand Down
Loading
Loading