Skip to content
Merged
Changes from 1 commit
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
21b7fb8
Initial plan
Copilot Aug 11, 2025
1860e84
Fix disposal issue by triggering OnDispose events in test execution
Copilot Aug 11, 2025
1710baf
Remove problematic test file and finalize disposal fix
Copilot Aug 11, 2025
75fc090
Remove dead code: ExtractTimeout, ExtractRetryCount, ExtractRepeatCount
thomhurst Aug 11, 2025
3004b59
Add timeout protection to OnDispose event invocation
thomhurst Aug 11, 2025
6e1ac4c
Fix hanging issue by preventing duplicate disposal handlers registration
Copilot Aug 12, 2025
35c5f2d
Merge branch 'main' into copilot/fix-2867
thomhurst Aug 12, 2025
644b9ca
Fix disposal hanging by removing disposal calls from OnDispose handlers
claude[bot] Aug 12, 2025
9fc1edd
Implement proper reference counting with disposal in ObjectTracker
claude[bot] Aug 12, 2025
7092303
Fix premature disposal of shared objects
claude[bot] Aug 12, 2025
5d6683b
Implement reference counting based disposal system
claude[bot] Aug 12, 2025
2e714d7
Fix disposal order to prevent ObjectDisposedException
claude[bot] Aug 12, 2025
7828979
Fix shared object disposal tracking to ensure all test contexts track…
claude[bot] Aug 12, 2025
e5da8c2
Fix ObjectDisposedException by preventing disposal of shared objects
claude[bot] Aug 12, 2025
7b139e3
Remove shared object concept and implement pure reference counting
claude[bot] Aug 12, 2025
6c2825f
Fix ObjectDisposedException by preventing disposal of shared objects
claude[bot] Aug 12, 2025
d6e8f76
Implement pure reference counting by removing all shared object detec…
claude[bot] Aug 12, 2025
b8ebc60
Implement pure reference counting disposal system as requested by user
Copilot Aug 12, 2025
8c01e6f
Merge branch 'main' into copilot/fix-2867
thomhurst Aug 12, 2025
02465d3
Fix race condition in object disposal by removing background task dis…
Copilot Aug 12, 2025
b21c07e
Restore global.json to original .NET 9.0 version
Copilot Aug 12, 2025
c4374ac
Merge branch 'main' into copilot/fix-2867
thomhurst Aug 12, 2025
78d31da
Fix hanging/deadlock issues in object disposal system
thomhurst Aug 12, 2025
11de084
Fix remaining build errors in disposal system
thomhurst Aug 12, 2025
6e5999a
Fix hanging/deadlock issues in object disposal system with recursive …
thomhurst Aug 12, 2025
f0a93f6
Fix object disposal issues and hanging tests
thomhurst Aug 12, 2025
c9f15a1
Fix object disposal tracking for shared objects in repeated tests
thomhurst Aug 12, 2025
a4fdd77
Fix object disposal tracking for shared objects in repeated tests
thomhurst Aug 13, 2025
947ff2c
Fix reference counting logic in object disposal tracking
thomhurst Aug 13, 2025
80b8cb8
Add debug logging and instance removal methods in object tracking and…
thomhurst Aug 13, 2025
ce1f16f
Refactor object retrieval methods to improve type handling and simpli…
thomhurst Aug 13, 2025
4991073
Enhance object disposal tracking and cleanup in ScopedDictionary
thomhurst Aug 13, 2025
0aaafcd
Change visibility of ObjectTracker methods to internal for better enc…
thomhurst Aug 13, 2025
b14a50c
Refactor property injection logic to improve task handling and object…
thomhurst Aug 13, 2025
44c2a20
Fix object disposal tracking for shared objects in repeated tests
thomhurst Aug 13, 2025
5c41935
Refactor TestBuilderContextAccessor visibility and enhance ScopedDict…
thomhurst Aug 13, 2025
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
Prev Previous commit
Next Next commit
Implement proper reference counting with disposal in ObjectTracker
- Restore ReleaseObject() calls in OnDispose handlers to actually dispose objects
- Objects are only disposed when reference count reaches zero (proper sharing)
- Add 5-second timeout protection to prevent hanging during disposal
- Separate DisposeObjectAsync() method for cleaner disposal logic
- Maintain duplicate handler prevention using _registeredHandlers
- Support both IAsyncDisposable and IDisposable objects
- Exception handling prevents disposal failures from hanging tests

This provides a generic and maintainable counting/tracking approach
that properly disposes shared objects while preventing race conditions.

Co-authored-by: Tom Longhurst <thomhurst@users.noreply.github.com>
  • Loading branch information
claude[bot] and thomhurst committed Aug 12, 2025
commit 9fc1edd3dfc2e9ca6a71311e0b3d634ff6602ff2
49 changes: 32 additions & 17 deletions TUnit.Core/Tracking/ObjectTracker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,27 +34,19 @@ public static void TrackObject(TestContextEvents events, object? obj)
{
events.OnDispose += async (_, _) =>
{
// Simply decrement the reference count without disposing to prevent hanging
// Shared objects (PerClass, PerAssembly, PerTestSession) will be disposed
// by their own lifecycle management when their scope ends
if (_trackedObjects.TryGetValue(obj, out var counter))
{
counter.Decrement();
}

await ReleaseObject(obj);
// Clean up the handler registration tracking
_registeredHandlers.TryRemove(handlerKey, out _);
};
}
}

/// <summary>
/// Decrements the reference count for an object and optionally disposes it.
/// This method is kept for potential future use but is not currently called
/// from OnDispose handlers to prevent hanging issues.
/// Decrements the reference count for an object and disposes it when count reaches zero.
/// Uses proper disposal pattern with async support and exception handling.
/// </summary>
/// <param name="obj">The object to release</param>
/// <returns>True if the object has no more references and was removed from tracking</returns>
/// <returns>Task representing the disposal operation</returns>
private static async Task ReleaseObject(object? obj)
{
if (obj == null)
Expand All @@ -69,20 +61,27 @@ private static async Task ReleaseObject(object? obj)

var count = counter.Decrement();

// Only dispose when reference count reaches zero
if (count <= 0)
{
_trackedObjects.TryRemove(obj, out _);

// Dispose the object without blocking
// Dispose the object with timeout to prevent hanging
try
{
if (obj is IAsyncDisposable asyncDisposable)
var disposeTask = DisposeObjectAsync(obj);
var timeoutTask = Task.Delay(TimeSpan.FromSeconds(5));
var completedTask = await Task.WhenAny(disposeTask, timeoutTask).ConfigureAwait(false);

if (completedTask == timeoutTask)
{
await asyncDisposable.DisposeAsync().ConfigureAwait(false);
// Timeout occurred, but don't throw to prevent hanging the test
// The object will be GC'd eventually
}
else if (obj is IDisposable disposable)
else
{
disposable.Dispose();
// Ensure any exceptions from the dispose task are observed
await disposeTask.ConfigureAwait(false);
}
}
catch
Expand Down Expand Up @@ -148,6 +147,22 @@ public static void Clear()
_trackedObjects.Clear();
}

/// <summary>
/// Disposes an object using the appropriate disposal method.
/// </summary>
/// <param name="obj">The object to dispose</param>
private static async Task DisposeObjectAsync(object obj)
{
if (obj is IAsyncDisposable asyncDisposable)
{
await asyncDisposable.DisposeAsync().ConfigureAwait(false);
}
else if (obj is IDisposable disposable)
{
disposable.Dispose();
}
}

/// <summary>
/// Determines if an object should be skipped from tracking.
/// </summary>
Expand Down
Loading