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

Extract Aspire.Hosting.Qdrant.Tests project #4879

Merged
merged 1 commit into from
Jul 15, 2024
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
7 changes: 7 additions & 0 deletions Aspire.sln
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Hosting", "Hosting", "{830A
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Hosting.Redis.Tests", "tests\Aspire.Hosting.Redis.Tests\Aspire.Hosting.Redis.Tests.csproj", "{1BC02557-B78B-48CE-9D3C-488A6B7672F4}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Hosting.Qdrant.Tests", "tests\Aspire.Hosting.Qdrant.Tests\Aspire.Hosting.Qdrant.Tests.csproj", "{8E2AA85E-C351-47B4-AF91-58557FAD5840}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -1326,6 +1328,10 @@ Global
{1BC02557-B78B-48CE-9D3C-488A6B7672F4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1BC02557-B78B-48CE-9D3C-488A6B7672F4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1BC02557-B78B-48CE-9D3C-488A6B7672F4}.Release|Any CPU.Build.0 = Release|Any CPU
{8E2AA85E-C351-47B4-AF91-58557FAD5840}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8E2AA85E-C351-47B4-AF91-58557FAD5840}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8E2AA85E-C351-47B4-AF91-58557FAD5840}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8E2AA85E-C351-47B4-AF91-58557FAD5840}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -1569,6 +1575,7 @@ Global
{C424395C-1235-41A4-BF55-07880A04368C} = {4981B3A5-4AFD-4191-BF7D-8692D9783D60}
{830A89EC-4029-4753-B25A-068BAE37DEC7} = {4981B3A5-4AFD-4191-BF7D-8692D9783D60}
{1BC02557-B78B-48CE-9D3C-488A6B7672F4} = {830A89EC-4029-4753-B25A-068BAE37DEC7}
{8E2AA85E-C351-47B4-AF91-58557FAD5840} = {830A89EC-4029-4753-B25A-068BAE37DEC7}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {6DCEDFEC-988E-4CB3-B45B-191EB5086E0C}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Net.Sockets;
using Aspire.Hosting.Qdrant;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Tests.Utils;
using Aspire.Hosting.Utils;
using Microsoft.Extensions.DependencyInjection;
using Xunit;

namespace Aspire.Hosting.Tests.Qdrant;
namespace Aspire.Hosting.Qdrant.Tests;

public class AddQdrantTests
{
Expand All @@ -22,7 +22,7 @@ public void AddQdrantAddsGeneratedApiKeyParameterWithUserSecretsParameterDefault

var qd = appBuilder.AddQdrant("qd");

Assert.IsType<UserSecretsParameterDefault>(qd.Resource.ApiKeyParameter.Default);
Assert.Equal("Aspire.Hosting.ApplicationModel.UserSecretsParameterDefault", qd.Resource.ApiKeyParameter.Default?.GetType().FullName);
}

[Fact]
Expand All @@ -32,7 +32,7 @@ public void AddQdrantDoesNotAddGeneratedPasswordParameterWithUserSecretsParamete

var qd = appBuilder.AddQdrant("qd");

Assert.IsNotType<UserSecretsParameterDefault>(qd.Resource.ApiKeyParameter.Default);
Assert.NotEqual("Aspire.Hosting.ApplicationModel.UserSecretsParameterDefault", qd.Resource.ApiKeyParameter.Default?.GetType().FullName);
}

[Fact]
Expand Down Expand Up @@ -177,7 +177,7 @@ public async Task QdrantClientAppWithReferenceContainsConnectionStrings()
.WithEndpoint("grpc", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 6334))
.WithEndpoint("http", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 6333));

var projectA = appBuilder.AddProject<ProjectA>("projecta")
var projectA = appBuilder.AddProject<ProjectA>("projecta", o => o.ExcludeLaunchProfile = true)
.WithReference(qdrant);

// Call environment variable callbacks.
Expand Down Expand Up @@ -314,7 +314,5 @@ public void AddQdrantWithSpecifyingPorts()
private sealed class ProjectA : IProjectMetadata
{
public string ProjectPath => "projectA";

public LaunchSettings LaunchSettings { get; } = new();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>$(NetCurrent)</TargetFramework>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Aspire.Hosting.AppHost\Aspire.Hosting.AppHost.csproj" />
<ProjectReference Include="..\..\src\Aspire.Hosting.Qdrant\Aspire.Hosting.Qdrant.csproj" />
<ProjectReference Include="..\..\src\Components\Aspire.Qdrant.Client\Aspire.Qdrant.Client.csproj" />
<ProjectReference Include="..\Aspire.Hosting.Tests\Aspire.Hosting.Tests.csproj" />
</ItemGroup>

<ItemGroup>
<Compile Include="$(RepoRoot)src\Aspire.Hosting.Qdrant\QdrantContainerImageTags.cs" />
<Compile Include="$(SharedDir)VolumeNameGenerator.cs" Link="Utils\VolumeNameGenerator.cs" />
</ItemGroup>

</Project>
218 changes: 218 additions & 0 deletions tests/Aspire.Hosting.Qdrant.Tests/QdrantFunctionalTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Aspire.Components.Common.Tests;
using Aspire.Hosting.Utils;
using Grpc.Core;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Polly;
using Qdrant.Client;
using Qdrant.Client.Grpc;
using Xunit;
using Xunit.Abstractions;

namespace Aspire.Hosting.Qdrant.Tests;

public class QdrantFunctionalTests(ITestOutputHelper testOutputHelper)
{
private const string CollectionName = "test_collection";
private static readonly float[] s_testVector = { 0.10022575f, -0.23998135f };

[Fact]
[RequiresDocker]
public async Task VerifyQdrantResource()
{
var cts = new CancellationTokenSource(TimeSpan.FromMinutes(3));

var builder = CreateDistributedApplicationBuilder();

var qdrant = builder.AddQdrant("qdrant");

using var app = builder.Build();

await app.StartAsync();

var hb = Host.CreateApplicationBuilder();

hb.Configuration.AddInMemoryCollection(new Dictionary<string, string?>
{
[$"ConnectionStrings:{qdrant.Resource.Name}"] = await qdrant.Resource.ConnectionStringExpression.GetValueAsync(default)
});

hb.AddQdrantClient(qdrant.Resource.Name);

using var host = hb.Build();

await host.StartAsync();

var qdrantClient = host.Services.GetRequiredService<QdrantClient>();

await CreateTestDataAsync(qdrantClient, cts.Token);

var results = await qdrantClient.SearchAsync(CollectionName, s_testVector, limit: 1, cancellationToken: cts.Token);
Assert.Collection(results,
r => Assert.Equal("Test", r.Payload["title"].StringValue));
}

private static async Task CreateTestDataAsync(QdrantClient qdrantClient, CancellationToken cancellationToken)
{
await qdrantClient.CreateCollectionAsync(CollectionName, new VectorParams { Size = 2, Distance = Distance.Cosine }, cancellationToken: cancellationToken);

var data = new[]
{
new PointStruct
{
Id = 1,
Vectors = s_testVector,
Payload =
{
["title"] = "Test"
}
}
};
var updateResult = await qdrantClient.UpsertAsync(CollectionName, data, cancellationToken: cancellationToken);
Assert.Equal(UpdateStatus.Completed, updateResult.Status);
}

[Theory]
[InlineData(true)]
[InlineData(false)]
[RequiresDocker]
public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume)
{
var cts = new CancellationTokenSource(TimeSpan.FromMinutes(3));
var pipeline = new ResiliencePipelineBuilder()
.AddRetry(new() { MaxRetryAttempts = 10, Delay = TimeSpan.FromSeconds(1), ShouldHandle = new PredicateBuilder().Handle<RpcException>() })
.Build();

string? volumeName = null;
string? bindMountPath = null;

try
{
var builder1 = CreateDistributedApplicationBuilder();
var qdrant1 = builder1.AddQdrant("qdrant");

if (useVolume)
{
// Use a deterministic volume name to prevent them from exhausting the machines if deletion fails
volumeName = VolumeNameGenerator.CreateVolumeName(qdrant1, nameof(WithDataShouldPersistStateBetweenUsages));

// if the volume already exists (because of a crashing previous run), try to delete it
DockerUtils.AttemptDeleteDockerVolume(volumeName);
qdrant1.WithDataVolume(volumeName);
}
else
{
bindMountPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
qdrant1.WithDataBindMount(bindMountPath);
}

using (var app = builder1.Build())
{
await app.StartAsync();
try
{
var hb = Host.CreateApplicationBuilder();

hb.Configuration.AddInMemoryCollection(new Dictionary<string, string?>
{
[$"ConnectionStrings:{qdrant1.Resource.Name}"] = await qdrant1.Resource.ConnectionStringExpression.GetValueAsync(default)
});

hb.AddQdrantClient(qdrant1.Resource.Name);

using (var host = hb.Build())
{
await host.StartAsync();

var qdrantClient = host.Services.GetRequiredService<QdrantClient>();

await CreateTestDataAsync(qdrantClient, cts.Token);
}
}
finally
{
// Stops the container, or the Volume/mount would still be in use
await app.StopAsync();
}
}

var builder2 = CreateDistributedApplicationBuilder();
var qdrant2 = builder2.AddQdrant("qdrant");

if (useVolume)
{
qdrant2.WithDataVolume(volumeName);
}
else
{
qdrant2.WithDataBindMount(bindMountPath!);
}

using (var app = builder2.Build())
{
await app.StartAsync();
try
{
var hb = Host.CreateApplicationBuilder();

hb.Configuration.AddInMemoryCollection(new Dictionary<string, string?>
{
[$"ConnectionStrings:{qdrant2.Resource.Name}"] = await qdrant2.Resource.ConnectionStringExpression.GetValueAsync(default)
});

hb.AddQdrantClient(qdrant2.Resource.Name);

using (var host = hb.Build())
{
await host.StartAsync();

var qdrantClient = host.Services.GetRequiredService<QdrantClient>();

await pipeline.ExecuteAsync(async token =>
{
var results = await qdrantClient.SearchAsync(CollectionName, s_testVector, limit: 1, cancellationToken: token);
Assert.Collection(results,
r => Assert.Equal("Test", r.Payload["title"].StringValue));
}, cts.Token);
}
}
finally
{
// Stops the container, or the Volume/mount would still be in use
await app.StopAsync();
}
}
}
finally
{
if (volumeName is not null)
{
DockerUtils.AttemptDeleteDockerVolume(volumeName);
}

if (bindMountPath is not null)
{
try
{
File.Delete(bindMountPath);
}
catch
{
// Don't fail test if we can't clean the temporary folder
}
}
}
}

private TestDistributedApplicationBuilder CreateDistributedApplicationBuilder()
{
var builder = TestDistributedApplicationBuilder.CreateWithTestContainerRegistry();
builder.Services.AddXunitLogging(testOutputHelper);
return builder;
}
}
12 changes: 9 additions & 3 deletions tests/Aspire.Hosting.Redis.Tests/RedisFunctionalTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using StackExchange.Redis;
using Xunit;
using Xunit.Abstractions;

namespace Aspire.Hosting.Redis.Tests;

public class RedisFunctionalTests
public class RedisFunctionalTests(ITestOutputHelper testOutputHelper)
{
[Fact]
[RequiresDocker]
Expand Down Expand Up @@ -293,6 +295,10 @@ public async Task PersistenceIsDisabledByDefault()
}
}

private static TestDistributedApplicationBuilder CreateDistributedApplicationBuilder() =>
TestDistributedApplicationBuilder.CreateWithTestContainerRegistry();
private TestDistributedApplicationBuilder CreateDistributedApplicationBuilder()
{
var builder = TestDistributedApplicationBuilder.CreateWithTestContainerRegistry();
builder.Services.AddXunitLogging(testOutputHelper);
return builder;
}
}
22 changes: 18 additions & 4 deletions tests/Aspire.Hosting.Tests/Utils/DockerUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,25 @@ public sealed class DockerUtils
{
public static void AttemptDeleteDockerVolume(string volumeName)
{
if (Process.Start("docker", $"volume rm {volumeName}") is { } process)
for (var i = 0; i < 3; i++)
{
process.WaitForExit(TimeSpan.FromSeconds(3));
process.Kill(entireProcessTree: true);
process.Dispose();
if (i != 0)
{
Thread.Sleep(1000);
}

if (Process.Start("docker", $"volume rm {volumeName}") is { } process)
{
var exited = process.WaitForExit(TimeSpan.FromSeconds(3));
var done = exited && process.ExitCode == 0;
process.Kill(entireProcessTree: true);
process.Dispose();

if (done)
{
break;
}
}
}
}
}
4 changes: 4 additions & 0 deletions tests/Shared/Logging/XunitLoggerFactoryExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Testing;
using Xunit.Abstractions;
using Xunit.Sdk;

namespace Microsoft.Extensions.Logging;

Expand Down Expand Up @@ -44,4 +45,7 @@ public static ILoggerFactory AddXunit(this ILoggerFactory loggerFactory, ITestOu
loggerFactory.AddProvider(new XunitLoggerProvider(output, minLevel, logStart));
return loggerFactory;
}

public static IServiceCollection AddXunitLogging(this IServiceCollection services, ITestOutputHelper output) =>
services.AddLogging(b => b.AddXunit(output));
}
Loading