Timeout Inheritance Change in NUnit

PR #5192 did not add Thread.Sleep in async code. The real case-study value is subtler: a release-branch merge changed timeout attribute inheritance, silently changing how derived test fixtures receive cancellation and timeout behavior.

nunit/nunitPR #5192
GCI0003COVERAGE GAPAsync TestsTimeoutsRule Design

70

files changed

37

commits merged

0

GCI0016 matches

What changed

PR #5192 was a release/4.6 merge to main. Among platform filter updates and framework additions, two attribute declarations changed Inherited = falseto Inherited = true: CancelAfterAttributeand TimeoutAttribute.

That means derived fixtures can begin inheriting cancellation or timeout behavior from a base class. The change may be correct, but it is externally observable for any test suite that depends on NUnit inheritance behavior.

Why this is risky

Timeout and cancellation metadata controls execution, not just documentation. A base fixture decorated with [CancelAfter]can now change the runtime behavior of every derived test. Long-running async tests may start receiving cancellation that they previously ignored.

The diff is deceptively small. A reviewer scanning for blocking calls will not see Thread.Sleepor .Wait(). The risk is encoded in attribute metadata.

What the original thin page got wrong

The old page claimed PR #5192 introduced Thread.Sleepand static mutable state. The verified diff does not support that. The closest file uses an existing AutoResetEvent.WaitOne()pattern in tests, not a newly added sleep.

Keeping this as a case study is still useful if it is framed honestly: it documents a rule-design gap and a class of behavior change that deserves a future detector.

Diff evidence

src/NUnitFramework/framework/Attributes
// CancelAfterAttribute now propagates from base fixtures/classes
-[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited=false)]
+[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public class CancelAfterAttribute : PropertyAttribute, IApplyToContext
// TimeoutAttribute also became inheritable
-[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Assembly, AllowMultiple = false, Inherited = false)]
+[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Assembly, AllowMultiple = false, Inherited = true)]
#if !NETFRAMEWORK
[Obsolete(".NET No longer supports aborting threads as it is not a safe thing to do...")]

What a better detector would ask

Coverage note
Signal   : a one-token attribute metadata change alters derived fixture execution behavior
Reality  : current GCI0016 would not fire; no Thread.Sleep, .Wait(), .Result, lock(this), or async void was added
Lesson   : some high-impact behavior changes are API metadata changes, not obvious code-body hazards

Important context

  • Current GCI0016 would not fire on this PR; there was no added Thread.Sleep, async blocking call, lock(this), or async void pattern.
  • The inheritance change appears intentional and may be a bug fix, not a regression.
  • This page is now a coverage-gap case study rather than a claim that GauntletCI would have blocked PR #5192.

Recommended review steps

  • Review inherited timeout/cancellation behavior with derived fixture tests before merging framework changes.
  • Add release notes for behavior changes caused by attribute metadata, even when method bodies do not change.
  • Consider a future rule for high-impact AttributeUsage changes on test framework attributes.

Sources

About the author

Eric Cogen -- Founder, GauntletCI

Eric Cogen is a senior .NET engineer with twenty years in production. He has shipped payments systems, internal platforms, and critical line-of-business applications — the kind where a 2 a.m. alert wasn't an emergency, it was a regular Tuesday. GauntletCI is the pre-commit checklist he wishes he had run before every commit.