pytest-xdist

repository·master·Indexed 23 days ago

https://github.com/pytest-dev/pytest-xdist

A pytest plugin for distributed testing, primarily focused on distributing tests across multiple CPUs to speed up execution. It provides various distribution modes (load, loadscope, loadfile, loadgroup, worksteal), worker management features like --ramp and --max-worker-restart, and tools for identifying worker processes via the worker_id fixture. It supports Python 3.14 and allows for custom worker count overrides via environment variables or Python interpreter options.

Tokens
10.3K
Snippets
18
Records
55
Agent score
73%

What's inside pytest-xdist

  1. Overview of pytest-xdist features

    master

    The pytest-xdist plugin provides several advanced execution modes:

    • Parallelization: Execute tests across multiple CPUs or remote hosts to speed up development or utilize special resources.
    • Multi-Platform coverage: Run tests in parallel across different Python interpreters or platforms. pytest will rsync your source code to remote locations, though it does not synchronize dependencies.
    • --looponfail (DEPRECATED): Repeatedly runs tests in a subprocess, waiting for file changes to re-run previously failing tests until they pass. Use caution as this feature is deprecated.
  2. How pytest-xdist works: Controller and Worker model

    master

    pytest-xdist operates using a controller and one or more workers.

    • The controller manages the test session, spawns workers, and distributes tests.
    • Each worker is a mini pytest runner responsible for performing its own full test collection and executing tests as instructed by the controller.

    Communication between the controller and workers is handled via execnet gateways, allowing workers to run in local or remote interpreters.

    Execution Flow

    1. Spawning: The controller spawns workers.
    2. Collection: Each worker performs a full test collection and sends the resulting test-ids back to the controller. The controller verifies that all workers collected the same tests in the same order.
    3. Indexing: To save bandwidth, the controller converts test-ids into simple indexes. Instead of sending a full test-id, the controller tells a worker to execute test index N.
    4. Distribution: Depending on the dist-mode, the controller sends tests to workers (see Distribution Modes).
    5. Execution: Workers wait for instructions. When they receive a test, they execute the pytest_runtest_protocol.
    6. Reporting: Workers send results back to the controller, which forwards them to standard pytest hooks like pytest_runtest_logstart and pytest_runtest_logreport. This ensures compatibility with plugins like junitxml.
    7. Shutdown: Once all tests are pending, the controller sends a "shutdown" signal. Workers finish their remaining tests and exit.
  3. Distribution Modes: each vs load

    master

    The way tests are distributed to workers depends on the dist-mode setting:

    • each: The controller sends the full list of test indexes to every node.
    • load: The controller uses a load-balancing approach. It initially sends approximately 25% of the tests to each worker in a round-robin fashion. The remaining tests are distributed dynamically as workers complete their current tasks, using heuristics like test duration and the worker's remaining queue size.
  4. Customize the number of workers for -n auto and -n logical

    master

    If you want to change how many processes auto or logical resolves to, you can use the following methods in order of priority:

    1. pytest hook: Implement pytest_xdist_auto_num_workers(config) in conftest.py. It can check config.option.numprocesses to see if the user requested "auto" or "logical". Returning None falls back to default.
    2. Environment Variable: Set PYTEST_XDIST_AUTO_NUM_WORKERS.
    3. Python Interpreter Option: Use -X cpu_count.
    4. Python Environment Variable: Set PYTHON_CPU_COUNT (standard for Python 3.13+).
  5. Override auto-detected CPU count for xdist workers

    master

    When using -n auto or -n logical, you can override the automatically detected number of workers using the PYTHON_CPU_COUNT environment variable or the -X cpu_count option (available in Python 3.13+, but supported on all versions via this mechanism).

    Note that the PYTEST_XDIST_AUTO_NUM_WORKERS environment variable takes precedence over both PYTHON_CPU_COUNT and the -X cpu_count option.

  6. Speed up test execution with pytest-xdist

    master

    The pytest-xdist plugin allows you to distribute tests across multiple CPUs to accelerate test execution. You can use the -n flag to specify the number of worker processes. Using auto will spawn a number of worker processes equal to the number of available CPUs, distributing tests randomly across them.

    pytest -n auto
  7. Run tests in a Python subprocess using --tx popen

    master

    You can use the --tx option with the popen executor to run tests in a separate Python subprocess. This is useful for testing against a specific Python interpreter version found in your system's PATH.

    To start a single subprocess using a specific interpreter (e.g., python3.9), use the syntax --tx popen//python=<interpreter_command>.

    pytest -d --tx popen//python=python3.9
  8. Run tests in parallel across multiple CPUs

    master

    You can speed up test execution by distributing tests across multiple CPUs using the -n flag. Using auto will spawn a number of worker processes equal to the number of available CPUs and distribute tests randomly across them.

    Note: The -s or --capture=no option is not compatible with pytest-xdist due to its implementation.

    pytest -n auto
  9. Make session-scoped fixtures execute only once

    master

    Because pytest-xdist runs each worker as a separate process, session scoped fixtures will execute once per worker. To ensure a fixture executes exactly once for the entire test session (e.g., for expensive data production or database initialization), use a file lock for inter-process communication.

    Recommended approach:

    1. Use tmp_path_factory.getbasetemp().parent to find a shared temporary directory.
    2. Use filelock.FileLock to coordinate access to a shared file.
    3. The first worker to acquire the lock produces the data and writes it to a file; subsequent workers read from that file.
    import json
    
    import pytest
    from filelock import FileLock
    
    
    @pytest.fixture(scope="session")
    def session_data(tmp_path_factory, worker_id):
        if worker_id == "master":
            # not executing in with multiple workers, just produce the data and let
            # pytest's fixture caching do its job
            return produce_expensive_data()
    
        # get the temp directory shared by all workers
        root_tmp_dir = tmp_path_factory.getbasetemp().parent
    
        fn = root_tmp_dir / "data.json"
        with FileLock(str(fn) + ".lock"):
            if fn.is_file():
                data = json.loads(fn.read_text())
            else:
                data = produce_expensive_data()
                fn.write_text(json.dumps(data))
        return data
  10. Run multiple workers on a remote machine using proxies

    master

    If you want to run multiple workers on a single remote machine, you can define a proxy gateway using the --px argument and then instruct workers to run via that proxy. This prevents the proxy itself from consuming a worker slot.

    In the example below, we declare a proxy gateway named my_proxy and create 5 workers that execute on the remote server through that proxy.

    pytest -d --px id=my_proxy//socket=192.168.1.102:8888 --tx 5*popen//via=my_proxy
  11. Send tests to remote SSH accounts using rsync

    master

    You can distribute tests to a remote SSH-reachable machine by synchronizing your package directory to the remote host.

    Note: The rsync feature is deprecated and scheduled for removal in release 4.0. For long-term stability, consider using SSH or socket servers.

    Requirements for successful execution:

    • Ensure all code and test directories are included via --rsyncdir.
    • Crucial: Every test (sub) directory must contain an __init__.py file. pytest-xdist references tests as fully qualified Python module paths; missing __init__.py files will cause setup errors on the remote side.

    Options:

    • --rsyncdir <dir>: Specify one or more directories to synchronize to the remote side.
    • --rsyncignore <pattern>: Specify one or more glob patterns to ignore during synchronization. Note that internal patterns .*, *.pyc, *.pyo, *~ cannot be overridden.
    pytest -d --rsyncdir mypkg --tx ssh=myhostpopen mypkg/tests/unit/test_something.py