diff --git a/TUnit.Example.FsCheck.TestProject/CancellationTokenBehaviourTests.cs b/TUnit.Example.FsCheck.TestProject/CancellationTokenBehaviourTests.cs
new file mode 100644
index 0000000000..d845049a7d
--- /dev/null
+++ b/TUnit.Example.FsCheck.TestProject/CancellationTokenBehaviourTests.cs
@@ -0,0 +1,89 @@
+using System.Diagnostics;
+using FsCheck;
+using FsCheck.Fluent;
+using TUnit.Core;
+using TUnit.FsCheck;
+
+namespace TUnit.Example.FsCheck.TestProject;
+
+///
+/// Verifies that FsCheck properties receive the TUnit timeout-backed CancellationToken
+/// rather than a token generated by FsCheck's default reflection-based arbitrary.
+///
+[Timeout(10_000)]
+public sealed class CancellationTokenBehaviourTests
+{
+ [Test, FsCheckProperty(MaxTest = 100)]
+ public bool CancellationTokenIsNotPreCancelled(CancellationToken cancellationToken)
+ {
+ return !cancellationToken.IsCancellationRequested;
+ }
+
+ [Test, FsCheckProperty(MaxTest = 1)]
+ public bool CancellationTokenIsTestContextToken(CancellationToken cancellationToken)
+ {
+ var expected = TestContext.Current?.Execution.CancellationToken ?? CancellationToken.None;
+ return cancellationToken == expected;
+ }
+
+ [Test, FsCheckProperty(MaxTest = 10, Arbitrary = [typeof(AlwaysNoneTokenArbitrary)])]
+ public bool UserSuppliedArbitraryOverridesDefault(CancellationToken cancellationToken)
+ {
+ return cancellationToken == CancellationToken.None;
+ }
+
+ [Test, FsCheckProperty(MaxTest = 10, Arbitrary = [typeof(PositiveIntArbitrary)])]
+ public bool UserArbitraryForDifferentTypeDoesNotBypassDefault(int x, CancellationToken cancellationToken)
+ {
+ return x > 0 && !cancellationToken.IsCancellationRequested;
+ }
+
+ [Test, FsCheckProperty(MaxTest = 1, Arbitrary = [])]
+ public bool EmptyArbitraryArrayStillUsesDefault(CancellationToken cancellationToken)
+ {
+ return !cancellationToken.IsCancellationRequested;
+ }
+
+#pragma warning disable TUnit0015 // intentionally omitted to verify the no-CT path
+ [Test, FsCheckProperty(MaxTest = 50)]
+ public bool NoCancellationTokenParameter_UnchangedBehaviour(int x, int y)
+ {
+ return x + y == y + x;
+ }
+#pragma warning restore TUnit0015
+
+ ///
+ /// [Timeout(500)] + an await on the CT for 5s. If the CT is the timeout token,
+ /// the await is cooperatively cancelled at ~500ms. Without the fix, FsCheck injected its
+ /// own token and the await either threw immediately (pre-cancelled) or ran to completion.
+ ///
+ [Test, FsCheckProperty(MaxTest = 1)]
+ [Timeout(500)]
+ public async Task CooperativeCancellationViaTimeout(CancellationToken cancellationToken)
+ {
+ var stopwatch = Stopwatch.StartNew();
+ try
+ {
+ await Task.Delay(5000, cancellationToken).ConfigureAwait(false);
+ return false;
+ }
+ catch (OperationCanceledException)
+ {
+ return stopwatch.Elapsed.TotalMilliseconds is >= 200 and <= 1_500;
+ }
+ }
+}
+
+///
+/// Test arbitrary that always returns .
+/// Used by
+/// to verify that user-supplied arbitraries take precedence over the default.
+///
+internal static class AlwaysNoneTokenArbitrary
+{
+ /// Always returns .
+ public static Arbitrary CancellationToken()
+ {
+ return Arb.From(Gen.Constant(System.Threading.CancellationToken.None));
+ }
+}
diff --git a/TUnit.FsCheck/CancellationTokenArbitrary.cs b/TUnit.FsCheck/CancellationTokenArbitrary.cs
new file mode 100644
index 0000000000..7467aeb7bf
--- /dev/null
+++ b/TUnit.FsCheck/CancellationTokenArbitrary.cs
@@ -0,0 +1,19 @@
+using FsCheck;
+using FsCheck.Fluent;
+using TUnit.Core;
+
+namespace TUnit.FsCheck;
+
+///
+/// Default for ; surfaces the
+/// timeout-backed token from .
+///
+internal static class CancellationTokenArbitrary
+{
+ public static Arbitrary CancellationToken()
+ {
+ // Called by FsCheck via reflection during property execution; TestContext.Current is in scope.
+ var token = TestContext.Current?.Execution.CancellationToken ?? System.Threading.CancellationToken.None;
+ return Arb.From(Gen.Constant(token));
+ }
+}
diff --git a/TUnit.FsCheck/FsCheckPropertyTestExecutor.cs b/TUnit.FsCheck/FsCheckPropertyTestExecutor.cs
index a2dc1e0fb9..1a56ce3d5b 100644
--- a/TUnit.FsCheck/FsCheckPropertyTestExecutor.cs
+++ b/TUnit.FsCheck/FsCheckPropertyTestExecutor.cs
@@ -87,6 +87,7 @@ private static MethodInfo GetMethodInfo(
return methods[0];
}
+ [DynamicDependency(DynamicallyAccessedMemberTypes.PublicMethods, typeof(CancellationTokenArbitrary))]
private Config CreateConfig()
{
var config = Config.QuickThrowOnFailure
@@ -105,10 +106,12 @@ private Config CreateConfig()
}
}
- if (_propertyAttribute.Arbitrary != null && _propertyAttribute.Arbitrary.Length > 0)
- {
- config = config.WithArbitrary(_propertyAttribute.Arbitrary);
- }
+ // Register a default Arbitrary that surfaces TestContext's
+ // timeout-backed token. User-supplied arbitraries are listed first; FsCheck's
+ // WithArbitrary resolves the first type in the list as highest priority, so
+ // user registrations override the default for conflicting types.
+ config = config.WithArbitrary(
+ (_propertyAttribute.Arbitrary ?? []).Append(typeof(CancellationTokenArbitrary)));
return config;
}