Per-scenario timeout

Behave provides a global --timeout flag but no way to set a different timeout per scenario. behave-kit fills that gap with tag-based overrides and platform-aware handlers.

Overview

  1. setup_timeout configures a default timeout on the context.

  2. Tags @timeout:N override the timeout per scenario or feature.

  3. On expiry the scenario fails with TimeoutError.

Platform notes

  • Unix (Linux, macOS): uses signal.SIGALRM for immediate interruption of the main thread.

  • Windows: signal.SIGALRM is unavailable, so a threading.Timer fallback is used. This cannot interrupt CPU-bound code — the timeout is detected after the current step finishes. I/O-bound code (time.sleep, socket reads, etc.) is interrupted promptly.

Usage

Wire the timeout hooks in your environment.py:

from behave_kit import setup_timeout
from behave_kit.timeout import timeout_before_scenario, timeout_after_scenario

def before_all(context):
    setup_timeout(context, default_timeout=30)

def before_scenario(context, scenario):
    timeout_before_scenario(context, scenario)

def after_scenario(context, scenario):
    timeout_after_scenario(context, scenario)

Environment variable

You can set the default timeout with the BEHAVE_SCENARIO_TIMEOUT environment variable. This is useful for CI runners or behave-runner integrations that control timeout from the command line:

BEHAVE_SCENARIO_TIMEOUT=30 behave

If setup_timeout(context) is called without default_timeout, the value is read from the environment. If the variable is not set, the default is 0 (no timeout).

Tag-based overrides

Override the default timeout per scenario or feature using @timeout:N tags:

@timeout:10
Scenario: Fast scenario with tag override
  When I do something quick

@timeout:0
Scenario: Disable timeout for this scenario
  When I do something slow

@timeout:60
Feature: Feature-level timeout inherits to all scenarios

Precedence rules:

  • Scenario tags take precedence over feature tags.

  • @timeout:0 disables the timeout for that scenario.

  • If no tag is present, the default timeout from setup_timeout is used.

  • If no default is configured (default_timeout=0), no timeout is applied.

Custom tag name

Use a custom tag name instead of the default timeout:

setup_timeout(context, default_timeout=30, timeout_tag="limit")

Tags would then be @limit:10, @limit:0, etc.

API reference

Per-scenario timeout with tag-based overrides.

Behave provides a global --timeout flag but no way to set a different timeout per scenario. This module fills that gap:

  1. setup_timeout configures a default timeout on the context.

  2. Tags @timeout:N override the timeout per scenario or feature.

  3. On expiry the scenario fails with TimeoutError.

Platform notes

  • Unix (Linux, macOS): uses signal.SIGALRM for immediate interruption of the main thread.

  • Windows: signal.SIGALRM is unavailable, so a threading.Timer fallback is used. This cannot interrupt CPU-bound code — the timeout is detected after the current step finishes. I/O-bound code (time.sleep, socket reads, etc.) is interrupted promptly because the timer callback sets a flag that __exit__ checks.

Usage in environment.py:

from behave_kit import setup_timeout
from behave_kit.timeout import timeout_before_scenario, timeout_after_scenario

def before_all(context):
    setup_timeout(context, default_timeout=30)

def before_scenario(context, scenario):
    timeout_before_scenario(context, scenario)

def after_scenario(context, scenario):
    timeout_after_scenario(context, scenario)
class behave_kit.timeout.SignalTimeoutHandler(timeout: float)[source]

Bases: object

Unix timeout handler using signal.SIGALRM.

Interrupts the main thread immediately when the deadline is reached. Only works on the main thread of the main interpreter.

class behave_kit.timeout.ThreadTimeoutHandler(timeout: float)[source]

Bases: object

Windows fallback timeout handler using threading.Timer.

Cannot interrupt CPU-bound code. The timeout is detected in __exit__ after the wrapped block finishes. I/O-bound code that checks for interrupts (e.g. time.sleep) may be interrupted sooner.

class behave_kit.timeout.TimeoutHandler(*args, **kwargs)[source]

Bases: Protocol

Protocol for platform-specific timeout handlers.

timeout: float
behave_kit.timeout.setup_timeout(context: Context, default_timeout: float | None = None, *, timeout_tag: str = 'timeout') None[source]

Configure per-scenario timeout.

Parameters:
  • context – The Behave context object.

  • default_timeout – Timeout in seconds for all scenarios. 0 disables the per-scenario timeout (Behave’s native --timeout still applies independently). If None, the value is read from the BEHAVE_SCENARIO_TIMEOUT environment variable (default 0).

  • timeout_tag – Name of the tag used for per-scenario overrides. The format is @<timeout_tag>:N where N is seconds. Default: "timeout" (i.e. @timeout:10).

behave_kit.timeout.timeout_after_scenario(context: Context, scenario: BehaveScenario) None[source]

Cancel the timeout timer for scenario.

Call this from after_scenario. Raises TimeoutError if the scenario exceeded its timeout (Windows fallback only; on Unix the error is raised immediately during step execution).

If the scenario already failed with an exception, that exception is passed to the handler so it doesn’t mask the original failure.

behave_kit.timeout.timeout_before_scenario(context: Context, scenario: BehaveScenario) None[source]

Start the timeout timer for scenario.

Call this from before_scenario. If no timeout is configured (default is 0 and no @timeout:N tag is present), this is a no-op.