Skip to content
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
17 changes: 17 additions & 0 deletions changelog.d/unreleased/1668.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
category: fixed
issues:
- 1668
affected:
- tests/CodeIndex.Tests/SqlitePoolSensitiveCollection.cs
- tests/CodeIndex.Tests/SqlitePoolCleanup.cs
- tests/CodeIndex.Tests/SqlitePoolSensitiveCollectionTests.cs
---

## English

- **SQLite pool-sensitive tests now clear pooled handles at collection boundaries (#1668)** — the serialized xUnit collection now owns a cleanup fixture that clears SQLite pools before and after the collection, reducing stale pooled-handle leakage after failed tests.

## 日本語

- **SQLite pool sensitive テストが collection 境界で pooled handle を解放するようになりました (#1668)** — 直列化された xUnit collection が cleanup fixture を持ち、collection の前後で SQLite pool を clear するため、失敗したテスト後に stale な pooled handle が残るリスクを減らします。
48 changes: 45 additions & 3 deletions tests/CodeIndex.Tests/SqlitePoolCleanup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ namespace CodeIndex.Tests;
internal static class SqlitePoolCleanup
{
private static readonly object Gate = new();
private static Action _clearAllPools = SqliteConnection.ClearAllPools;
private static int _activeExclusiveOwners;
private static bool _clearPending;

Expand All @@ -23,15 +24,38 @@ internal static void ClearPoolsForWindowsFileRelease(bool callerOwnsExclusiveAcc
if (!OperatingSystem.IsWindows())
return;

ClearPools(callerOwnsExclusiveAccess, deferForActiveOwners: true);
}

internal static void ClearPoolsAtCollectionBoundary()
{
ClearPools(callerOwnsExclusiveAccess: true, deferForActiveOwners: false);
}

internal static IDisposable ReplaceClearAllPoolsForTesting(Action clearAllPools)
{
ArgumentNullException.ThrowIfNull(clearAllPools);

lock (Gate)
{
if (_activeExclusiveOwners > 0 && !callerOwnsExclusiveAccess)
var prior = _clearAllPools;
_clearAllPools = clearAllPools;
return new RestoreClearAllPools(prior);
}
}

private static void ClearPools(bool callerOwnsExclusiveAccess, bool deferForActiveOwners)
{
lock (Gate)
{
if (deferForActiveOwners && _activeExclusiveOwners > 0 && !callerOwnsExclusiveAccess)
{
_clearPending = true;
return;
}

SqliteConnection.ClearAllPools();
_clearPending = false;
_clearAllPools();
}
}

Expand All @@ -51,9 +75,27 @@ public void Dispose()
if (_activeExclusiveOwners == 0 && _clearPending)
{
_clearPending = false;
SqliteConnection.ClearAllPools();
_clearAllPools();
}
}
}
}

private sealed class RestoreClearAllPools(Action prior) : IDisposable
{
private bool _disposed;

public void Dispose()
{
lock (Gate)
{
if (_disposed)
return;

_disposed = true;
_clearAllPools = prior;
_clearPending = false;
}
}
}
}
17 changes: 16 additions & 1 deletion tests/CodeIndex.Tests/SqlitePoolSensitiveCollection.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
namespace CodeIndex.Tests;

[CollectionDefinition("SQLite pool sensitive", DisableParallelization = true)]
public sealed class SqlitePoolSensitiveCollection
public sealed class SqlitePoolSensitiveCollection : ICollectionFixture<SqlitePoolSensitiveFixture>
{
}

public sealed class SqlitePoolSensitiveFixture : IAsyncLifetime
{
public Task InitializeAsync()
{
SqlitePoolCleanup.ClearPoolsAtCollectionBoundary();
return Task.CompletedTask;
}

public Task DisposeAsync()
{
SqlitePoolCleanup.ClearPoolsAtCollectionBoundary();
return Task.CompletedTask;
}
}
40 changes: 40 additions & 0 deletions tests/CodeIndex.Tests/SqlitePoolSensitiveCollectionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
namespace CodeIndex.Tests;

[Collection("SQLite pool sensitive")]
public sealed class SqlitePoolSensitiveCollectionTests
{
[Fact]
public void Collection_RegistersPoolCleanupFixture()
{
Assert.Contains(
typeof(SqlitePoolSensitiveCollection).GetInterfaces(),
static type => type.IsGenericType &&
type.GetGenericTypeDefinition() == typeof(ICollectionFixture<>) &&
type.GetGenericArguments()[0] == typeof(SqlitePoolSensitiveFixture));
}

[Fact]
public async Task Fixture_ClearsPoolsOnInitializeAndDispose()
{
var clearCount = 0;
using var _ = SqlitePoolCleanup.ReplaceClearAllPoolsForTesting(() => clearCount++);
var fixture = new SqlitePoolSensitiveFixture();

await fixture.InitializeAsync();
await fixture.DisposeAsync();

Assert.Equal(2, clearCount);
}

[Fact]
public void CollectionBoundaryClear_DoesNotDeferBehindActiveExclusiveOwner()
{
var clearCount = 0;
using var _ = SqlitePoolCleanup.ReplaceClearAllPoolsForTesting(() => clearCount++);
using var owner = SqlitePoolCleanup.EnterExclusiveOwner();

SqlitePoolCleanup.ClearPoolsAtCollectionBoundary();

Assert.Equal(1, clearCount);
}
}
Loading