From cedc2c5e4f9b90282f11453546eb15640f8d8231 Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Fri, 17 Apr 2026 17:36:59 +0100
Subject: [PATCH 1/2] fix: restore [Obsolete] members removed in v1.27 (#5539)
PR #5384 deleted previously [Obsolete]-marked public APIs in a minor
release, breaking semver. Restore them with [Obsolete] reapplied so v1.x
consumers can upgrade without compile errors. Actual deletion is
deferred to the v2 major bump (tracked in #5604).
Restored:
- TUnit.Assertions: CountWrapper, LengthWrapper, HasCount/HasLength
overloads on CollectionAssertionBase / AssertionExtensions
- TUnit.Core: ObjectBag on TestBuilderContext + TestRegisteredContext,
Timing record, ITestOutput.Timings + RecordTiming bridged to internal
TimingEntry storage with new _timingsLock for user-facing concurrent
RecordTiming calls
- PublicAPI snapshots regenerated for net8/9/10/472
---
.../Conditions/Wrappers/CountWrapper.cs | 288 ++++++++++++++++++
.../Conditions/Wrappers/LengthWrapper.cs | 93 ++++++
.../Extensions/AssertionExtensions.cs | 27 ++
.../Sources/CollectionAssertionBase.cs | 27 ++
TUnit.Core/Contexts/TestRegisteredContext.cs | 6 +
TUnit.Core/Interfaces/ITestOutput.cs | 15 +
TUnit.Core/TestBuilderContext.cs | 6 +
TUnit.Core/TestContext.Output.cs | 19 ++
TUnit.Core/Timing.cs | 7 +
...Has_No_API_Changes.DotNet10_0.verified.txt | 32 ++
..._Has_No_API_Changes.DotNet8_0.verified.txt | 32 ++
..._Has_No_API_Changes.DotNet9_0.verified.txt | 32 ++
...ary_Has_No_API_Changes.Net4_7.verified.txt | 32 ++
...Has_No_API_Changes.DotNet10_0.verified.txt | 20 ++
..._Has_No_API_Changes.DotNet8_0.verified.txt | 20 ++
..._Has_No_API_Changes.DotNet9_0.verified.txt | 20 ++
...ary_Has_No_API_Changes.Net4_7.verified.txt | 20 ++
17 files changed, 696 insertions(+)
create mode 100644 TUnit.Assertions/Conditions/Wrappers/CountWrapper.cs
create mode 100644 TUnit.Assertions/Conditions/Wrappers/LengthWrapper.cs
create mode 100644 TUnit.Core/Timing.cs
diff --git a/TUnit.Assertions/Conditions/Wrappers/CountWrapper.cs b/TUnit.Assertions/Conditions/Wrappers/CountWrapper.cs
new file mode 100644
index 0000000000..2c06445e74
--- /dev/null
+++ b/TUnit.Assertions/Conditions/Wrappers/CountWrapper.cs
@@ -0,0 +1,288 @@
+using System.Collections;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Text;
+using TUnit.Assertions.Conditions;
+using TUnit.Assertions.Core;
+using TUnit.Assertions.Extensions;
+
+namespace TUnit.Assertions.Conditions.Wrappers;
+
+///
+/// Wrapper for collection count assertions that provides .EqualTo() method.
+/// Example: await Assert.That(list).Count().EqualTo(5);
+///
+public class CountWrapper : IAssertionSource
+ where TCollection : IEnumerable
+{
+ private readonly AssertionContext _context;
+
+ public CountWrapper(AssertionContext context)
+ {
+ _context = context;
+ }
+
+ AssertionContext IAssertionSource.Context => _context;
+
+ ///
+ /// Not supported on CountWrapper - use IsTypeOf on the assertion source before calling HasCount().
+ ///
+ TypeOfAssertion IAssertionSource.IsTypeOf()
+ {
+ throw new NotSupportedException(
+ "IsTypeOf is not supported after HasCount(). " +
+ "Use: Assert.That(value).IsTypeOf>().HasCount().EqualTo(5)");
+ }
+
+ ///
+ /// Not supported on CountWrapper - use IsAssignableTo on the assertion source before calling HasCount().
+ ///
+ IsAssignableToAssertion IAssertionSource.IsAssignableTo()
+ {
+ throw new NotSupportedException(
+ "IsAssignableTo is not supported after HasCount(). " +
+ "Use: Assert.That(value).IsAssignableTo>().HasCount().EqualTo(5)");
+ }
+
+ ///
+ /// Not supported on CountWrapper - use IsNotAssignableTo on the assertion source before calling HasCount().
+ ///
+ IsNotAssignableToAssertion IAssertionSource.IsNotAssignableTo()
+ {
+ throw new NotSupportedException(
+ "IsNotAssignableTo is not supported after HasCount(). " +
+ "Use: Assert.That(value).IsNotAssignableTo>().HasCount().EqualTo(5)");
+ }
+
+ ///
+ /// Not supported on CountWrapper - use IsAssignableFrom on the assertion source before calling HasCount().
+ ///
+ IsAssignableFromAssertion IAssertionSource.IsAssignableFrom()
+ {
+ throw new NotSupportedException(
+ "IsAssignableFrom is not supported after HasCount(). " +
+ "Use: Assert.That(value).IsAssignableFrom>().HasCount().EqualTo(5)");
+ }
+
+ ///
+ /// Not supported on CountWrapper - use IsNotAssignableFrom on the assertion source before calling HasCount().
+ ///
+ IsNotAssignableFromAssertion IAssertionSource.IsNotAssignableFrom()
+ {
+ throw new NotSupportedException(
+ "IsNotAssignableFrom is not supported after HasCount(). " +
+ "Use: Assert.That(value).IsNotAssignableFrom>().HasCount().EqualTo(5)");
+ }
+
+ ///
+ /// Not supported on CountWrapper - use IsNotTypeOf on the assertion source before calling HasCount().
+ ///
+ IsNotTypeOfAssertion IAssertionSource.IsNotTypeOf()
+ {
+ throw new NotSupportedException(
+ "IsNotTypeOf is not supported after HasCount(). " +
+ "Use: Assert.That(value).IsNotTypeOf>().HasCount().EqualTo(5)");
+ }
+
+ ///
+ /// Asserts that the collection count is equal to the expected count.
+ ///
+ public CollectionCountAssertion EqualTo(
+ int expectedCount,
+ [CallerArgumentExpression(nameof(expectedCount))] string? expression = null)
+ {
+ _context.ExpressionBuilder.Append($".EqualTo({expression})");
+ return new CollectionCountAssertion(_context, expectedCount);
+ }
+
+ ///
+ /// Asserts that the collection count is greater than or equal to the expected count.
+ ///
+ public TValue_IsGreaterThanOrEqualTo_TValue_Assertion GreaterThanOrEqualTo(
+ int expected,
+ [CallerArgumentExpression(nameof(expected))] string? expression = null)
+ {
+ _context.ExpressionBuilder.Append($".GreaterThanOrEqualTo({expression})");
+ // Map context to get the count
+ var countContext = _context.Map(value =>
+ {
+ if (value == null)
+ {
+ return 0;
+ }
+
+ if (value is ICollection collection)
+ {
+ return collection.Count;
+ }
+
+ return value.Cast