Skip to content

Commit a5c8dac

Browse files
committed
tests
1 parent 41b4f36 commit a5c8dac

File tree

6 files changed

+208
-71
lines changed

6 files changed

+208
-71
lines changed

.github/workflows/ci.yml

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,80 @@ jobs:
4040
token: ${{ secrets.CODECOV_TOKEN }}
4141
files: ./**/coverage.cobertura.xml
4242
fail_ci_if_error: false
43+
44+
cosmos-emulator:
45+
name: Cosmos Emulator Tests
46+
runs-on: windows-latest
47+
env:
48+
DOTNET_VERSION: '9.0.x'
49+
SOLUTION_FILE: GraphRag.slnx
50+
COSMOS_EMULATOR_CONNECTION_STRING: AccountEndpoint=https://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==
51+
GRAPHRAG_SKIP_TESTCONTAINERS: '1'
52+
53+
steps:
54+
- name: Checkout
55+
uses: actions/checkout@v5
56+
57+
- name: Setup .NET
58+
uses: actions/setup-dotnet@v4
59+
with:
60+
dotnet-version: ${{ env.DOTNET_VERSION }}
61+
62+
- name: Install Cosmos DB Emulator
63+
shell: pwsh
64+
run: choco install azure-cosmosdb-emulator --yes --no-progress
65+
66+
- name: Start Cosmos DB Emulator
67+
shell: pwsh
68+
run: |
69+
$emulatorPath = "${env:ProgramFiles}\Azure Cosmos DB Emulator\CosmosDB.Emulator.exe"
70+
if (-not (Test-Path $emulatorPath)) {
71+
throw "Cosmos DB Emulator executable not found at $emulatorPath"
72+
}
73+
74+
Start-Process -FilePath $emulatorPath -ArgumentList "/EnablePreview","/NoUI","/DisableTelemetry","/Start" -Wait:$false
75+
76+
$certPath = Join-Path $env:TEMP "cosmos-emulator.pem"
77+
$maxAttempts = 60
78+
for ($attempt = 0; $attempt -lt $maxAttempts; $attempt++) {
79+
try {
80+
Invoke-WebRequest -Uri https://localhost:8081/_explorer/emulator.pem -OutFile $certPath -SkipCertificateCheck
81+
break
82+
} catch {
83+
Start-Sleep -Seconds 2
84+
}
85+
}
86+
87+
if ($attempt -eq $maxAttempts) {
88+
throw "Cosmos DB Emulator did not start within expected time."
89+
}
90+
91+
"COSMOS_EMULATOR_CERT_PATH=$certPath" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
92+
93+
- name: Import Cosmos Emulator Certificate
94+
shell: pwsh
95+
run: |
96+
$certPemPath = $env:COSMOS_EMULATOR_CERT_PATH
97+
if (-not (Test-Path $certPemPath)) {
98+
throw "Emulator certificate not found at $certPemPath"
99+
}
100+
101+
$pemContent = Get-Content $certPemPath -Raw
102+
$certificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]::CreateFromPem($pemContent)
103+
$certBytes = $certificate.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert)
104+
$certFile = Join-Path $env:TEMP "cosmos-emulator.cer"
105+
[System.IO.File]::WriteAllBytes($certFile, $certBytes)
106+
107+
Import-Certificate -FilePath $certFile -CertStoreLocation Cert:\LocalMachine\Root | Out-Null
108+
109+
- name: Restore dependencies
110+
shell: pwsh
111+
run: dotnet restore $env:SOLUTION_FILE
112+
113+
- name: Build
114+
shell: pwsh
115+
run: dotnet build $env:SOLUTION_FILE --configuration Release --no-restore
116+
117+
- name: Test Cosmos scenarios
118+
shell: pwsh
119+
run: dotnet test $env:SOLUTION_FILE --configuration Release --no-build --verbosity normal --filter "Category=Cosmos"

src/ManagedCode.GraphRag.Postgres/PostgresGraphStore.cs

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,20 @@ public PostgresGraphStore(PostgresGraphStoreOptions options, ILogger<PostgresGra
3737
{
3838
ArgumentNullException.ThrowIfNull(options);
3939

40-
_connectionString = options.ConnectionString ?? throw new ArgumentNullException(nameof(options.ConnectionString));
41-
_graphName = options.GraphName ?? throw new ArgumentNullException(nameof(options.GraphName));
40+
var connectionString = options.ConnectionString;
41+
if (string.IsNullOrWhiteSpace(connectionString))
42+
{
43+
throw new ArgumentException("ConnectionString cannot be null or whitespace.", nameof(options));
44+
}
45+
46+
var graphName = options.GraphName;
47+
if (string.IsNullOrWhiteSpace(graphName))
48+
{
49+
throw new ArgumentException("GraphName cannot be null or whitespace.", nameof(options));
50+
}
51+
52+
_connectionString = connectionString;
53+
_graphName = graphName;
4254
_graphNameLiteral = BuildGraphNameLiteral(_graphName);
4355
_autoCreateIndexes = options.AutoCreateIndexes;
4456
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
@@ -86,7 +98,11 @@ public async Task UpsertNodeAsync(string id, string label, IReadOnlyDictionary<s
8698
var propertyAssignments = BuildPropertyAssignments("n", ConvertProperties(properties), parameters, "node_prop");
8799

88100
var queryBuilder = new StringBuilder();
89-
queryBuilder.Append($"MERGE (n:{EscapeLabel(label)} {{ id: ${CypherParameterNames.NodeId} }})");
101+
queryBuilder.Append("MERGE (n:");
102+
queryBuilder.Append(EscapeLabel(label));
103+
queryBuilder.Append(" { id: $");
104+
queryBuilder.Append(CypherParameterNames.NodeId);
105+
queryBuilder.Append(" })");
90106

91107
if (propertyAssignments.Count > 0)
92108
{
@@ -121,9 +137,15 @@ public async Task UpsertRelationshipAsync(string sourceId, string targetId, stri
121137
var propertyAssignments = BuildPropertyAssignments("rel", ConvertProperties(properties), parameters, "rel_prop");
122138

123139
var queryBuilder = new StringBuilder();
124-
queryBuilder.Append($"MATCH (source {{ id: ${CypherParameterNames.SourceId} }}), (target {{ id: ${CypherParameterNames.TargetId} }})");
140+
queryBuilder.Append("MATCH (source { id: $");
141+
queryBuilder.Append(CypherParameterNames.SourceId);
142+
queryBuilder.Append(" }), (target { id: $");
143+
queryBuilder.Append(CypherParameterNames.TargetId);
144+
queryBuilder.Append(" })");
125145
queryBuilder.AppendLine();
126-
queryBuilder.Append($"MERGE (source)-[rel:{EscapeLabel(type)}]->(target)");
146+
queryBuilder.Append("MERGE (source)-[rel:");
147+
queryBuilder.Append(EscapeLabel(type));
148+
queryBuilder.Append("]->(target)");
127149

128150
if (propertyAssignments.Count > 0)
129151
{

src/ManagedCode.GraphRag/Cache/MemoryPipelineCache.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,15 @@ public Task ClearAsync(CancellationToken cancellationToken = default)
7171
{
7272
cancellationToken.ThrowIfCancellationRequested();
7373

74+
var scopePrefix = string.Concat(_scope, ":");
75+
7476
foreach (var cacheKey in _keys.Keys)
7577
{
78+
if (!cacheKey.StartsWith(scopePrefix, StringComparison.Ordinal))
79+
{
80+
continue;
81+
}
82+
7683
_memoryCache.Remove(cacheKey);
7784
_keys.TryRemove(cacheKey, out _);
7885
}
@@ -84,7 +91,7 @@ public IPipelineCache CreateChild(string name)
8491
{
8592
ArgumentException.ThrowIfNullOrWhiteSpace(name);
8693
var childScope = string.Concat(_scope, ":", name);
87-
return new MemoryPipelineCache(_memoryCache, childScope, new ConcurrentDictionary<string, byte>());
94+
return new MemoryPipelineCache(_memoryCache, childScope, _keys);
8895
}
8996

9097
private string GetCacheKey(string key)

tests/ManagedCode.GraphRag.Tests/Cache/MemoryPipelineCacheTests.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,20 @@ public async Task ChildCache_IsolatedFromParent()
4242
Assert.False(await parent.HasAsync("value"));
4343
Assert.Equal("child", await child.GetAsync("value"));
4444
}
45+
46+
[Fact]
47+
public async Task ClearAsync_RemovesChildEntries()
48+
{
49+
var memoryCache = new MemoryCache(new MemoryCacheOptions());
50+
var parent = new MemoryPipelineCache(memoryCache);
51+
var child = parent.CreateChild("child");
52+
53+
await parent.SetAsync("parentValue", "parent");
54+
await child.SetAsync("childValue", "child");
55+
56+
await parent.ClearAsync();
57+
58+
Assert.False(await parent.HasAsync("parentValue"));
59+
Assert.False(await child.HasAsync("childValue"));
60+
}
4561
}

tests/ManagedCode.GraphRag.Tests/Integration/GraphRagApplicationFixture.cs

Lines changed: 79 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -31,31 +31,42 @@ public sealed class GraphRagApplicationFixture : IAsyncLifetime
3131

3232
public async Task InitializeAsync()
3333
{
34-
_neo4jContainer = new Neo4jBuilder()
35-
.WithImage("neo4j:5.23.0-community")
36-
.WithEnvironment("NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes")
37-
.WithEnvironment("NEO4J_PLUGINS", "[\"apoc\"]")
38-
.WithEnvironment("NEO4J_dbms_default__listen__address", "0.0.0.0")
39-
.WithEnvironment("NEO4J_dbms_default__advertised__address", "localhost")
40-
.WithEnvironment("NEO4J_AUTH", $"neo4j/{Neo4jPassword}")
41-
.WithWaitStrategy(Wait.ForUnixContainer().UntilInternalTcpPortIsAvailable(7687))
42-
.Build();
43-
44-
_postgresContainer = new PostgreSqlBuilder()
45-
.WithImage("apache/age:latest")
46-
.WithDatabase(PostgresDatabase)
47-
.WithUsername("postgres")
48-
.WithPassword(PostgresPassword)
49-
.WithCleanUp(true)
50-
.WithWaitStrategy(Wait.ForUnixContainer().UntilInternalTcpPortIsAvailable(5432))
51-
.Build();
52-
53-
await Task.WhenAll(_neo4jContainer.StartAsync(), _postgresContainer.StartAsync()).ConfigureAwait(false);
54-
55-
await EnsurePostgresDatabaseAsync().ConfigureAwait(false);
56-
57-
var boltEndpoint = new Uri(_neo4jContainer.GetConnectionString(), UriKind.Absolute);
58-
var postgresConnection = _postgresContainer.GetConnectionString();
34+
var skipContainers = string.Equals(
35+
Environment.GetEnvironmentVariable("GRAPHRAG_SKIP_TESTCONTAINERS"),
36+
"1",
37+
StringComparison.OrdinalIgnoreCase);
38+
39+
Uri? boltEndpoint = null;
40+
string? postgresConnection = null;
41+
42+
if (!skipContainers)
43+
{
44+
_neo4jContainer = new Neo4jBuilder()
45+
.WithImage("neo4j:5.23.0-community")
46+
.WithEnvironment("NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes")
47+
.WithEnvironment("NEO4J_PLUGINS", "[\"apoc\"]")
48+
.WithEnvironment("NEO4J_dbms_default__listen__address", "0.0.0.0")
49+
.WithEnvironment("NEO4J_dbms_default__advertised__address", "localhost")
50+
.WithEnvironment("NEO4J_AUTH", $"neo4j/{Neo4jPassword}")
51+
.WithWaitStrategy(Wait.ForUnixContainer().UntilInternalTcpPortIsAvailable(7687))
52+
.Build();
53+
54+
_postgresContainer = new PostgreSqlBuilder()
55+
.WithImage("apache/age:latest")
56+
.WithDatabase(PostgresDatabase)
57+
.WithUsername("postgres")
58+
.WithPassword(PostgresPassword)
59+
.WithCleanUp(true)
60+
.WithWaitStrategy(Wait.ForUnixContainer().UntilInternalTcpPortIsAvailable(5432))
61+
.Build();
62+
63+
await Task.WhenAll(_neo4jContainer.StartAsync(), _postgresContainer.StartAsync()).ConfigureAwait(false);
64+
await EnsurePostgresDatabaseAsync().ConfigureAwait(false);
65+
66+
boltEndpoint = new Uri(_neo4jContainer.GetConnectionString(), UriKind.Absolute);
67+
postgresConnection = _postgresContainer.GetConnectionString();
68+
}
69+
5970
var cosmosConnectionString = Environment.GetEnvironmentVariable("COSMOS_EMULATOR_CONNECTION_STRING");
6071
var includeCosmos = !string.IsNullOrWhiteSpace(cosmosConnectionString);
6172

@@ -69,39 +80,55 @@ public async Task InitializeAsync()
6980

7081
services.AddGraphRag();
7182

72-
services.AddKeyedSingleton<WorkflowDelegate>("neo4j-seed", static (_, _) => async (config, context, token) =>
83+
if (!skipContainers && boltEndpoint is not null && postgresConnection is not null)
7384
{
74-
var graph = context.Services.GetRequiredKeyedService<IGraphStore>("neo4j");
75-
await graph.InitializeAsync(token).ConfigureAwait(false);
76-
await graph.UpsertNodeAsync("alice", "Person", new Dictionary<string, object?> { ["name"] = "Alice" }, token).ConfigureAwait(false);
77-
await graph.UpsertNodeAsync("bob", "Person", new Dictionary<string, object?> { ["name"] = "Bob" }, token).ConfigureAwait(false);
78-
await graph.UpsertRelationshipAsync("alice", "bob", "KNOWS", new Dictionary<string, object?> { ["since"] = 2024 }, token).ConfigureAwait(false);
79-
var relationships = new List<GraphRelationship>();
80-
await foreach (var relationship in graph.GetOutgoingRelationshipsAsync("alice", token).ConfigureAwait(false))
85+
services.AddKeyedSingleton<WorkflowDelegate>("neo4j-seed", static (_, _) => async (config, context, token) =>
8186
{
82-
relationships.Add(relationship);
83-
}
87+
var graph = context.Services.GetRequiredKeyedService<IGraphStore>("neo4j");
88+
await graph.InitializeAsync(token).ConfigureAwait(false);
89+
await graph.UpsertNodeAsync("alice", "Person", new Dictionary<string, object?> { ["name"] = "Alice" }, token).ConfigureAwait(false);
90+
await graph.UpsertNodeAsync("bob", "Person", new Dictionary<string, object?> { ["name"] = "Bob" }, token).ConfigureAwait(false);
91+
await graph.UpsertRelationshipAsync("alice", "bob", "KNOWS", new Dictionary<string, object?> { ["since"] = 2024 }, token).ConfigureAwait(false);
92+
var relationships = new List<GraphRelationship>();
93+
await foreach (var relationship in graph.GetOutgoingRelationshipsAsync("alice", token).ConfigureAwait(false))
94+
{
95+
relationships.Add(relationship);
96+
}
8497

85-
context.Items["neo4j:relationship-count"] = relationships.Count;
86-
return new WorkflowResult(null);
87-
});
98+
context.Items["neo4j:relationship-count"] = relationships.Count;
99+
return new WorkflowResult(null);
100+
});
88101

89-
services.AddKeyedSingleton<WorkflowDelegate>("postgres-seed", static (_, _) => async (config, context, token) =>
90-
{
91-
var graph = context.Services.GetRequiredKeyedService<IGraphStore>("postgres");
92-
await graph.InitializeAsync(token).ConfigureAwait(false);
93-
await graph.UpsertNodeAsync("chapter-1", "Chapter", new Dictionary<string, object?> { ["title"] = "Origins" }, token).ConfigureAwait(false);
94-
await graph.UpsertNodeAsync("chapter-2", "Chapter", new Dictionary<string, object?> { ["title"] = "Discovery" }, token).ConfigureAwait(false);
95-
await graph.UpsertRelationshipAsync("chapter-1", "chapter-2", "LEADS_TO", new Dictionary<string, object?> { ["weight"] = 0.9 }, token).ConfigureAwait(false);
96-
var relationships = new List<GraphRelationship>();
97-
await foreach (var relationship in graph.GetOutgoingRelationshipsAsync("chapter-1", token).ConfigureAwait(false))
102+
services.AddKeyedSingleton<WorkflowDelegate>("postgres-seed", static (_, _) => async (config, context, token) =>
98103
{
99-
relationships.Add(relationship);
100-
}
104+
var graph = context.Services.GetRequiredKeyedService<IGraphStore>("postgres");
105+
await graph.InitializeAsync(token).ConfigureAwait(false);
106+
await graph.UpsertNodeAsync("chapter-1", "Chapter", new Dictionary<string, object?> { ["title"] = "Origins" }, token).ConfigureAwait(false);
107+
await graph.UpsertNodeAsync("chapter-2", "Chapter", new Dictionary<string, object?> { ["title"] = "Discovery" }, token).ConfigureAwait(false);
108+
await graph.UpsertRelationshipAsync("chapter-1", "chapter-2", "LEADS_TO", new Dictionary<string, object?> { ["weight"] = 0.9 }, token).ConfigureAwait(false);
109+
var relationships = new List<GraphRelationship>();
110+
await foreach (var relationship in graph.GetOutgoingRelationshipsAsync("chapter-1", token).ConfigureAwait(false))
111+
{
112+
relationships.Add(relationship);
113+
}
101114

102-
context.Items["postgres:relationship-count"] = relationships.Count;
103-
return new WorkflowResult(null);
104-
});
115+
context.Items["postgres:relationship-count"] = relationships.Count;
116+
return new WorkflowResult(null);
117+
});
118+
119+
services.AddNeo4jGraphStore("neo4j", options =>
120+
{
121+
options.Uri = boltEndpoint.ToString();
122+
options.Username = "neo4j";
123+
options.Password = Neo4jPassword;
124+
}, makeDefault: true);
125+
126+
services.AddPostgresGraphStore("postgres", options =>
127+
{
128+
options.ConnectionString = postgresConnection!;
129+
options.GraphName = "graphrag";
130+
});
131+
}
105132

106133
if (includeCosmos)
107134
{
@@ -123,19 +150,6 @@ public async Task InitializeAsync()
123150
});
124151
}
125152

126-
services.AddNeo4jGraphStore("neo4j", options =>
127-
{
128-
options.Uri = boltEndpoint.ToString();
129-
options.Username = "neo4j";
130-
options.Password = Neo4jPassword;
131-
}, makeDefault: true);
132-
133-
services.AddPostgresGraphStore("postgres", options =>
134-
{
135-
options.ConnectionString = postgresConnection;
136-
options.GraphName = "graphrag";
137-
});
138-
139153
if (includeCosmos)
140154
{
141155
services.AddCosmosGraphStore("cosmos", options =>

tests/ManagedCode.GraphRag.Tests/Integration/GraphStoreIntegrationTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ public async Task GraphStores_RoundTripRelationshipsAsync(string providerKey)
5656
}
5757

5858
[Fact]
59+
[Trait("Category", "Cosmos")]
5960
public async Task CosmosGraphStore_RoundTrips_WhenEmulatorAvailable()
6061
{
6162
var cosmosStore = fixture.Services.GetKeyedService<IGraphStore>("cosmos");

0 commit comments

Comments
 (0)