Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Dec 19, 2025

[Timeout] attribute on [Before(Assembly)], [Before(Class)], [Before(TestSession)], and [Before(TestDiscovery)] hooks was ignored—always using the default 5-minute timeout regardless of the specified value.

[Timeout(900_000)] // 15 minutes - WAS IGNORED, always used 300000ms
[Before(Assembly)]
public static async Task SetUp(CancellationToken cancellationToken)
{
    await Task.Delay(TimeSpan.FromMinutes(6), cancellationToken);
}

Root Cause

ProcessHookRegistrationAsync (which invokes TimeoutAttribute.OnHookRegistered) was only called for test hooks. The delegate creation methods for class/assembly/session/discovery hooks were synchronous and bypassed hook registration entirely.

Changes

  • Added async delegate creation methods (CreateClassHookDelegateAsync, CreateAssemblyHookDelegateAsync, etc.) that call ProcessHookRegistrationAsync
  • Updated all hook building methods to use the async creators
  • Converted CollectBefore/AfterClassHooksAsync and CollectBefore/AfterAssemblyHooksAsync to properly process hook registration events

Verification

Both execution modes (Source Generated and Reflection) now respect hook timeouts:

Hook 'ClassHookTimeoutTests.BeforeClass(cancellationToken)' exceeded timeout of 100ms
Original prompt

This section details on the original issue you should resolve

<issue_title>[Bug]: TimeoutAttribute on [Before(Assembly)] hook is ignored - default 5 minute timeout always used</issue_title>
<issue_description>### Description

The [Timeout] attribute placed on a [Before(Assembly)] hook method is not being applied. The hook always uses the default 5-minute timeout (300,000ms) defined in HookMethod.cs, regardless of the timeout value specified in the attribute.

Expected Behavior

The hook should use the 15-minute timeout specified in the [Timeout(900_000)] attribute.

Actual Behavior

The hook fails with:

TUnit.Engine.Exceptions.TestFailedException: BeforeAssemblyException: BeforeAssembly hook failed:
Hook 'IntegrationTestsBase.SetUp()' exceeded timeout of 300000ms

The timeout is always 300,000ms (5 minutes) - the default value from HookMethod.Timeout.

Steps to Reproduce

  1. Create a test class with a [Before(Assembly)] hook that takes longer than 5 minutes on slow CI/CD environments
  2. Add [Timeout(900_000)] attribute to increase timeout to 15 minutes
  3. Run the tests
public abstract class IntegrationTestsBase                                                                                                                                     
{                                                                                                                                                                              
    [Timeout(900_000)] // 15 minutes - THIS IS IGNORED                                                                                                                         
    [Before(Assembly)]                                                                                                                                                         
    public static async Task SetUp(CancellationToken cancellationToken)                                                                                                        
    {                                                                                                                                                                          
        // Long-running setup (starting containers, etc.)                                                                                                                      
        await Task.Delay(TimeSpan.FromMinutes(6), cancellationToken);                                                                                                          
    }                                                                                                                                                                          
}                                                                                                                                                                              

TUnit Version

1.5.80

.NET Version

.NET 10.0

Operating System

Linux

IDE / Test Runner

dotnet CLI (dotnet test / dotnet run)

Error Output / Stack Tra...


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI changed the title [WIP] Fix ignored timeout attribute on before assembly hook Fix TimeoutAttribute not being applied to non-test hooks Dec 19, 2025
Copilot AI requested a review from thomhurst December 19, 2025 20:34
Copilot AI and others added 2 commits January 6, 2026 19:48
The TimeoutAttribute on [Before(Assembly)], [Before(Class)], [Before(TestSession)],
and [Before(TestDiscovery)] hooks was being ignored because ProcessHookRegistrationAsync
was not being called for these hooks. This commit:

- Adds async versions of hook delegate creation methods that call ProcessHookRegistrationAsync
- Updates InitializeAsync to await the async hook building methods
- Converts CollectBeforeAssemblyHooksAsync, CollectAfterAssemblyHooksAsync,
  CollectBeforeClassHooksAsync, and CollectAfterClassHooksAsync to properly
  process hook registration events
- Adds HookTimeoutTests to verify the fix works for class and assembly hooks

Co-authored-by: thomhurst <[email protected]>
@thomhurst thomhurst force-pushed the copilot/fix-timeout-attribute-issue branch from 4dfaece to 61eede3 Compare January 6, 2026 19:48
@thomhurst thomhurst marked this pull request as ready for review January 6, 2026 19:48
Copilot AI review requested due to automatic review settings January 6, 2026 19:48
@thomhurst thomhurst enabled auto-merge (squash) January 6, 2026 19:48
@thomhurst
Copy link
Owner

Summary

This PR fixes a bug where the [Timeout] attribute on assembly, class, test session, and test discovery hooks was ignored, always using the default 5-minute timeout.

Critical Issues

None found ✅

Analysis

Root Cause

The bug occurred because ProcessHookRegistrationAsync (which invokes TimeoutAttribute.OnHookRegistered) was only called for test-level hooks. Assembly, class, session, and discovery hooks used synchronous delegate creation methods that bypassed hook registration entirely.

Solution

The PR correctly implements the fix by:

  1. Creating async delegate creation methods that call ProcessHookRegistrationAsync:

    • CreateClassHookDelegateAsync (lines 728-744)
    • CreateAssemblyHookDelegateAsync (lines 757-773)
    • CreateTestSessionHookDelegateAsync (lines 786-802)
    • CreateBeforeTestDiscoveryHookDelegateAsync (lines 815-831)
    • CreateTestDiscoveryHookDelegateAsync (lines 844-860)
  2. Updating initialization to use async methods (lines 50-60)

  3. Converting collection methods to properly process hook registration:

    • CollectBeforeClassHooksAsyncBuildBeforeClassHooksAsync (lines 425-490)
    • CollectAfterClassHooksAsyncBuildAfterClassHooksAsync (lines 492-554)
    • CollectBeforeAssemblyHooksAsyncBuildBeforeAssemblyHooksAsync (lines 556-582)
    • CollectAfterAssemblyHooksAsyncBuildAfterAssemblyHooksAsync (lines 584-610)

Dual-Mode Compliance (Rule 1) ✅

The changes only affect TUnit.Engine (reflection mode). Source-generated mode works differently but produces the same runtime behavior:

  • Source-gen mode: Hooks are pre-generated at compile time with Body delegates that directly wrap user code. When hooks are collected from Sources collections at runtime, they go through the same ProcessHookRegistrationAsync flow during initialization (lines 50-60).

  • Reflection mode: Hooks are discovered via reflection and delegates are created during collection. The fix ensures ProcessHookRegistrationAsync is called during delegate creation.

Both modes converge at the same runtime path in HookCollectionService.InitializeAsync, where all hooks (regardless of origin) now properly call ProcessHookRegistrationAsync through the new async delegate creators.

Test Coverage ✅

The test class HookTimeoutTests(TestMode testMode) properly parameterizes on TestMode, ensuring both source-generated and reflection modes are tested. The test project includes concrete test cases in TUnit.TestProject/HookTimeoutTests.cs with:

  • ClassHookTimeoutTests.BeforeClass with 100ms timeout (should fail with 500ms delay)
  • AssemblyHookTimeoutPassTests.BeforeAssembly with 2000ms timeout (should pass with 50ms delay)

Performance Considerations

The conversion from synchronous to async delegate creation is appropriate here because:

  • Hook collection happens once during initialization, not in hot paths
  • ProcessHookRegistrationAsync needs to invoke event receivers which may be async
  • The async overhead is negligible compared to test execution time

Verdict

APPROVE - No critical issues

The fix correctly addresses the root cause, maintains dual-mode compatibility, includes proper test coverage, and follows TUnit's architectural patterns.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a bug where the [Timeout] attribute was being ignored on non-test hooks ([Before(Assembly)], [Before(Class)], [Before(TestSession)], and [Before(TestDiscovery)]), causing them to always use the default 5-minute timeout regardless of the specified value.

Key Changes:

  • Added async delegate creation methods in the reflection engine to invoke ProcessHookRegistrationAsync, which triggers TimeoutAttribute.OnHookRegistered
  • Updated all hook building methods to use the new async creators instead of synchronous ones
  • Added integration and unit tests to verify timeout behavior for class and assembly hooks

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
TUnit.Engine/Services/HookCollectionService.cs Added async delegate creation methods (CreateClassHookDelegateAsync, CreateAssemblyHookDelegateAsync, etc.) that call ProcessHookRegistrationAsync to ensure timeout attributes are processed; updated all hook collection methods to await these async creators
TUnit.TestProject/HookTimeoutTests.cs Added integration tests with class and assembly hooks that have timeouts to verify the fix works in both timeout success and failure scenarios
TUnit.Engine.Tests/HookTimeoutTests.cs Added unit tests that verify hook timeout behavior across both source-generated and reflection execution modes

Comment on lines +7 to +17
public class HookTimeoutTests
{
/// <summary>
/// A 100ms timeout on the hook - it should fail because the hook takes 500ms
/// </summary>
[Test]
public void Test_WithTimeoutHook()
{
// This test exists to verify that the hook timeout was applied correctly
}
}
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test class HookTimeoutTests contains a test method but has no hooks defined. The test Test_WithTimeoutHook doesn't verify anything meaningful since there are no hooks with timeouts in this class. Either:

  1. Add a test-level hook with a timeout to this class (e.g., [Timeout(100)] [Before(Test)]), or
  2. Remove this class entirely since the other test classes (ClassHookTimeoutTests and AssemblyHookTimeoutPassTests) already provide coverage for hook timeouts.

The comment on line 10 suggests this test should verify hook timeout behavior, but without an actual hook, it cannot fulfill that purpose.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: TimeoutAttribute on [Before(Assembly)] hook is ignored - default 5 minute timeout always used

2 participants